Security Headers and CORS Configuration
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.