Skip to main content
ship before you scale

Security Headers and CORS Configuration

2 min read Chapter 17 of 42

Security Headers and CORS Configuration

The Feature

Marketflow returns security headers on every response that instruct browsers to enable built-in protections, and CORS is configured to allow only the legitimate frontend origin.

The Decision

Security headers are free defense. They are HTTP response headers that tell the browser to enable protections like XSS filtering, frame embedding prevention, and content type sniffing prevention. Not setting them leaves the browser’s defaults in place, which are less restrictive.

The Implementation

Security Headers Middleware

# backend/app/middleware/security_headers.py
from collections.abc import Callable

from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware


class SecurityHeadersMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next: Callable) -> Response:
        response = await call_next(request)

        response.headers["X-Content-Type-Options"] = "nosniff"
        response.headers["X-Frame-Options"] = "DENY"
        response.headers["X-XSS-Protection"] = "0"  # Disabled; CSP replaces it
        response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
        response.headers["Permissions-Policy"] = (
            "camera=(), microphone=(), geolocation=()"
        )

        if not request.url.path.startswith("/api/docs"):
            response.headers["Content-Security-Policy"] = (
                "default-src 'self'; "
                "script-src 'self'; "
                "style-src 'self' 'unsafe-inline'; "
                "img-src 'self' data: https:; "
                "connect-src 'self' https://*.supabase.co https://api.stripe.com"
            )

        return response

X-Content-Type-Options: nosniff prevents the browser from interpreting a JSON response as HTML (which could execute scripts). X-Frame-Options: DENY prevents the application from being embedded in an iframe (clickjacking protection). Referrer-Policy controls how much URL information is shared when navigating to external links.

CORS Configuration

# TRAP: Overly permissive CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # Any website can make requests to the API
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
# allow_origins=["*"] with allow_credentials=True is a security
# vulnerability. Any website can make authenticated requests to the API.
# SAFE: Explicit origins for each environment
from app.config import settings

allowed_origins = ["https://marketflow.app"]
if settings.is_development:
    allowed_origins.extend([
        "http://localhost:5173",
        "https://dev.marketflow.app",
    ])

app.add_middleware(
    CORSMiddleware,
    allow_origins=allowed_origins,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
)

The production configuration allows only https://marketflow.app. The development configuration adds localhost:5173 (Vite dev server) and the Cloudflare Tunnel hostname. No wildcards. No blanket permissions.

The Trap

The most common CORS mistake is setting allow_origins=["*"] during development and forgetting to change it before deployment. The Docker Compose environment uses the same config.py as production. The is_development flag controls which origins are allowed. There is no separate “development CORS config” that could be accidentally deployed.

The Cost

Security headers and CORS configuration add zero runtime cost. They are static response headers set on every response. The implementation time is approximately 30 minutes.