Security Overview
This document provides an OWASP-aligned security checklist for the Spotlight platform. Each control maps to the corresponding implementation in the codebase.
OWASP Compliance Checklist
A01: Broken Access Control
| Control | Status | Implementation |
|---|---|---|
| API key authentication on all endpoints | Done | ApiKeyService validates every request |
| JWT authentication on all endpoints | Done | JwtValidator with per-tenant JWKS |
| Admin endpoints require admin role | Done | require_admin dependency using pluggable AdminStrategy |
| Tenant data isolation | Done | DynamoDB partition key isolation (tenant_id prefix) |
| Origin lockdown per tenant | Done | check_origin_allowed validates Origin header |
| No direct object reference exposure | Done | All queries scoped by tenant_id |
See Authentication and Data Isolation for details.
A02: Cryptographic Failures
| Control | Status | Implementation |
|---|---|---|
| API keys stored as SHA-256 hashes | Done | hashlib.sha256 -- plaintext never stored |
| Constant-time hash comparison | Done | secrets.compare_digest prevents timing attacks |
| HTTPS enforced | Done | CloudFront redirect-to-https, HSTS header |
| TLS 1.2+ minimum | Done | CloudFront TLSv1.2_2021, HSTS max-age=63072000 |
| DynamoDB encryption at rest | Done | Terraform server_side_encryption { enabled = true } |
| JWT signature validation | Done | python-jose with JWKS key set |
| No secrets in logs | Done | Structured logging with structlog -- keys never logged |
A03: Injection
| Control | Status | Implementation |
|---|---|---|
| Input validation via Pydantic | Done | All request bodies validated by Pydantic BaseModel |
| Max length constraints on all string fields | Done | max_length=MAX_SELECTOR_LENGTH on selectors, content |
| DynamoDB parameterized queries | Done | Expression attribute values (no string concatenation) |
| No SQL/NoSQL injection surface | Done | DynamoDB API uses typed attribute maps |
| CSS selector validation | Done | ElementTarget validates selector format |
| URL pattern validation | Done | UrlPattern validates URL patterns |
A04: Insecure Design
| Control | Status | Implementation |
|---|---|---|
| Least-privilege IAM policies | Done | Lambda roles scoped to specific table ARNs |
| Defense in depth (API key + JWT + origin) | Done | Three-layer authentication |
| Rate limiting at API Gateway | Done | throttling_rate_limit and throttling_burst_limit |
| Circuit breaker for emergency disable | Done | CircuitBreakerToggledByAdmin admin endpoint |
| Audit trail for all admin actions | Done | AdminActionEvent with before/after snapshots |
| Event sourcing via outbox pattern | Done | Transactional consistency for domain events |
A05: Security Misconfiguration
| Control | Status | Implementation |
|---|---|---|
| Security headers on all responses | Done | SecurityHeadersMiddleware ASGI middleware |
| CORS restricted to configured origins | Done | API Gateway and application-level CORS |
| No debug mode in production | Done | ENVIRONMENT check guards debug features |
| Demo auth disabled in production | Done | Gated on ALLOW_DEMO_AUTH env var; left unset in prod |
| S3 public access blocked | Done | block_public_acls, block_public_policy |
| Infrastructure as Code (Terraform) | Done | No manual configuration drift |
A06: Vulnerable and Outdated Components
| Control | Status | Implementation |
|---|---|---|
| Dependency scanning | Done | safety package in dev dependencies |
| Pinned dependency versions | Done | pyproject.toml with locked versions |
| Python 3.13 runtime | Done | Latest stable Python for Lambda |
A07: Identification and Authentication Failures
| Control | Status | Implementation |
|---|---|---|
| JWKS-based JWT validation | Done | JwtValidator with 1-hour cache + auto-invalidation |
| Key rotation support | Done | Cache invalidation on validation failure |
| API key prefix indexing | Done | 12-char prefix for fast lookup without exposing key |
| Negative results not cached | Done | Prevents cache-poisoning attacks |
| Multi-strategy admin detection | Done | TableLookupStrategy, JwtPermissionStrategy, JwtRoleStrategy |
A08: Software and Data Integrity Failures
| Control | Status | Implementation |
|---|---|---|
| Immutable tour versions | Done | Tours.Versions table with append-only writes |
| Outbox pattern for event integrity | Done | Transactional write with application state |
| DynamoDB point-in-time recovery | Done | PITR enabled in production |
| S3 bucket versioning | Done | SDK assets bucket versioned |
| Audit trail with before/after snapshots | Done | create_admin_event captures full diffs |
A09: Security Logging and Monitoring Failures
| Control | Status | Implementation |
|---|---|---|
| Structured logging (structlog) | Done | JSON-formatted logs with context |
| OpenTelemetry instrumentation | Done | Traces, metrics, logs via OTLP |
| API Gateway access logging | Done | Request-level CloudWatch logs |
| Admin action audit trail | Done | Audit.AdminActions table with GSIs |
| Failed auth attempt logging | Done | jwt.decode_failed, api_key.hash_mismatch log events |
| EventBridge dead letter queue | Done | SQS DLQ with 14-day retention |
A10: Server-Side Request Forgery (SSRF)
| Control | Status | Implementation |
|---|---|---|
| JWKS URLs from trusted config only | Done | jwks_url from tenant config, not user input |
| HTTP client timeouts | Done | httpx.AsyncClient with 10-second timeout |
| No user-controlled URL fetching | Done | No endpoint accepts arbitrary URLs for server-side fetch |
Security Headers
The SecurityHeadersMiddleware adds the following headers to every HTTP response:
python
SECURITY_HEADERS = [
("x-content-type-options", "nosniff"),
("x-frame-options", "DENY"),
("strict-transport-security", "max-age=63072000; includeSubDomains; preload"),
("x-xss-protection", "1; mode=block"),
("referrer-policy", "strict-origin-when-cross-origin"),
("cache-control", "no-store"),
("content-security-policy", "default-src 'none'; frame-ancestors 'none'"),
]| Header | Purpose |
|---|---|
X-Content-Type-Options: nosniff | Prevents MIME type sniffing |
X-Frame-Options: DENY | Prevents clickjacking via iframes |
Strict-Transport-Security | Forces HTTPS for 2 years with preload |
X-XSS-Protection | Enables browser XSS filter |
Referrer-Policy | Limits referrer information leakage |
Cache-Control: no-store | Prevents caching of API responses |
Content-Security-Policy | Restrictive CSP for API responses |
Rate Limiting
Rate limiting is enforced at two levels:
API Gateway Level
hcl
default_route_settings {
# Prod values; dev uses 100 / 50.
throttling_rate_limit = 1000 # requests/second
throttling_burst_limit = 500 # burst capacity
}Application Level
The API returns X-RateLimit-Remaining headers and responds with 429 Too Many Requests when limits are exceeded.
Input Validation
All request bodies are validated using Pydantic models with explicit constraints:
python
class ElementTarget(BaseModel):
data_attribute: str | None = Field(max_length=MAX_SELECTOR_LENGTH)
element_id: str | None = Field(max_length=MAX_SELECTOR_LENGTH)
css_selector: str | None = Field(max_length=MAX_SELECTOR_LENGTH)
description: str | None = Field(max_length=200)
model_config = {"frozen": True} # immutable after creation
@model_validator(mode="after")
def at_least_one_selector(self) -> ElementTarget:
if not any([self.data_attribute, self.element_id, self.css_selector]):
raise ValueError("At least one targeting method required")
return selfKey validation patterns:
max_lengthconstraints on all string fields.frozen = Trueon value objects for immutability.- Custom validators for business rules (e.g., "at least one selector required").
- Pydantic
BaseModelrejects unknown fields by default.
Reporting Security Issues
If you discover a security vulnerability, please report it responsibly. Do not open a public issue.