Skip to content

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

ControlStatusImplementation
API key authentication on all endpointsDoneApiKeyService validates every request
JWT authentication on all endpointsDoneJwtValidator with per-tenant JWKS
Admin endpoints require admin roleDonerequire_admin dependency using pluggable AdminStrategy
Tenant data isolationDoneDynamoDB partition key isolation (tenant_id prefix)
Origin lockdown per tenantDonecheck_origin_allowed validates Origin header
No direct object reference exposureDoneAll queries scoped by tenant_id

See Authentication and Data Isolation for details.

A02: Cryptographic Failures

ControlStatusImplementation
API keys stored as SHA-256 hashesDonehashlib.sha256 -- plaintext never stored
Constant-time hash comparisonDonesecrets.compare_digest prevents timing attacks
HTTPS enforcedDoneCloudFront redirect-to-https, HSTS header
TLS 1.2+ minimumDoneCloudFront TLSv1.2_2021, HSTS max-age=63072000
DynamoDB encryption at restDoneTerraform server_side_encryption { enabled = true }
JWT signature validationDonepython-jose with JWKS key set
No secrets in logsDoneStructured logging with structlog -- keys never logged

A03: Injection

ControlStatusImplementation
Input validation via PydanticDoneAll request bodies validated by Pydantic BaseModel
Max length constraints on all string fieldsDonemax_length=MAX_SELECTOR_LENGTH on selectors, content
DynamoDB parameterized queriesDoneExpression attribute values (no string concatenation)
No SQL/NoSQL injection surfaceDoneDynamoDB API uses typed attribute maps
CSS selector validationDoneElementTarget validates selector format
URL pattern validationDoneUrlPattern validates URL patterns

A04: Insecure Design

ControlStatusImplementation
Least-privilege IAM policiesDoneLambda roles scoped to specific table ARNs
Defense in depth (API key + JWT + origin)DoneThree-layer authentication
Rate limiting at API GatewayDonethrottling_rate_limit and throttling_burst_limit
Circuit breaker for emergency disableDoneCircuitBreakerToggledByAdmin admin endpoint
Audit trail for all admin actionsDoneAdminActionEvent with before/after snapshots
Event sourcing via outbox patternDoneTransactional consistency for domain events

A05: Security Misconfiguration

ControlStatusImplementation
Security headers on all responsesDoneSecurityHeadersMiddleware ASGI middleware
CORS restricted to configured originsDoneAPI Gateway and application-level CORS
No debug mode in productionDoneENVIRONMENT check guards debug features
Demo auth disabled in productionDoneGated on ALLOW_DEMO_AUTH env var; left unset in prod
S3 public access blockedDoneblock_public_acls, block_public_policy
Infrastructure as Code (Terraform)DoneNo manual configuration drift

A06: Vulnerable and Outdated Components

ControlStatusImplementation
Dependency scanningDonesafety package in dev dependencies
Pinned dependency versionsDonepyproject.toml with locked versions
Python 3.13 runtimeDoneLatest stable Python for Lambda

A07: Identification and Authentication Failures

ControlStatusImplementation
JWKS-based JWT validationDoneJwtValidator with 1-hour cache + auto-invalidation
Key rotation supportDoneCache invalidation on validation failure
API key prefix indexingDone12-char prefix for fast lookup without exposing key
Negative results not cachedDonePrevents cache-poisoning attacks
Multi-strategy admin detectionDoneTableLookupStrategy, JwtPermissionStrategy, JwtRoleStrategy

A08: Software and Data Integrity Failures

ControlStatusImplementation
Immutable tour versionsDoneTours.Versions table with append-only writes
Outbox pattern for event integrityDoneTransactional write with application state
DynamoDB point-in-time recoveryDonePITR enabled in production
S3 bucket versioningDoneSDK assets bucket versioned
Audit trail with before/after snapshotsDonecreate_admin_event captures full diffs

A09: Security Logging and Monitoring Failures

ControlStatusImplementation
Structured logging (structlog)DoneJSON-formatted logs with context
OpenTelemetry instrumentationDoneTraces, metrics, logs via OTLP
API Gateway access loggingDoneRequest-level CloudWatch logs
Admin action audit trailDoneAudit.AdminActions table with GSIs
Failed auth attempt loggingDonejwt.decode_failed, api_key.hash_mismatch log events
EventBridge dead letter queueDoneSQS DLQ with 14-day retention

A10: Server-Side Request Forgery (SSRF)

ControlStatusImplementation
JWKS URLs from trusted config onlyDonejwks_url from tenant config, not user input
HTTP client timeoutsDonehttpx.AsyncClient with 10-second timeout
No user-controlled URL fetchingDoneNo 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'"),
]
HeaderPurpose
X-Content-Type-Options: nosniffPrevents MIME type sniffing
X-Frame-Options: DENYPrevents clickjacking via iframes
Strict-Transport-SecurityForces HTTPS for 2 years with preload
X-XSS-ProtectionEnables browser XSS filter
Referrer-PolicyLimits referrer information leakage
Cache-Control: no-storePrevents caching of API responses
Content-Security-PolicyRestrictive 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 self

Key validation patterns:

  • max_length constraints on all string fields.
  • frozen = True on value objects for immutability.
  • Custom validators for business rules (e.g., "at least one selector required").
  • Pydantic BaseModel rejects unknown fields by default.

Reporting Security Issues

If you discover a security vulnerability, please report it responsibly. Do not open a public issue.

Spotlight