forked from cardosofelipe/fast-next-template
Enhance security headers middleware with configurable CSP modes
- Introduced `CSP_MODE` setting with `strict`, `relaxed`, and `disabled` options for flexible content security policies. - Updated middleware to apply varying CSP rules based on mode and request context (e.g., API docs). - Improved security and frontend compatibility with detailed policy definitions for external resources. - Defaulted `CSP_MODE` to `relaxed`, ensuring balanced security and usability in modern applications.
This commit is contained in:
@@ -15,6 +15,15 @@ class Settings(BaseSettings):
|
|||||||
description="Environment: development, staging, or production"
|
description="Environment: development, staging, or production"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Security: Content Security Policy
|
||||||
|
# Set to False to disable CSP entirely (not recommended)
|
||||||
|
# Set to True for strict CSP (blocks most external resources)
|
||||||
|
# Set to "relaxed" for modern frontend development
|
||||||
|
CSP_MODE: str = Field(
|
||||||
|
default="relaxed",
|
||||||
|
description="CSP mode: 'strict', 'relaxed', or 'disabled'"
|
||||||
|
)
|
||||||
|
|
||||||
# Database configuration
|
# Database configuration
|
||||||
POSTGRES_USER: str = "postgres"
|
POSTGRES_USER: str = "postgres"
|
||||||
POSTGRES_PASSWORD: str = "postgres"
|
POSTGRES_PASSWORD: str = "postgres"
|
||||||
|
|||||||
@@ -72,7 +72,14 @@ app.add_middleware(
|
|||||||
# Add security headers middleware
|
# Add security headers middleware
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def add_security_headers(request: Request, call_next):
|
async def add_security_headers(request: Request, call_next):
|
||||||
"""Add security headers to all responses"""
|
"""
|
||||||
|
Add security headers to all responses.
|
||||||
|
|
||||||
|
CSP modes (set via CSP_MODE env var):
|
||||||
|
- 'strict': Maximum security, blocks most external resources (default for production)
|
||||||
|
- 'relaxed': Allows common CDNs and modern frontend features (default for development)
|
||||||
|
- 'disabled': No CSP (not recommended, use only for debugging)
|
||||||
|
"""
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
|
|
||||||
# Prevent clickjacking
|
# Prevent clickjacking
|
||||||
@@ -89,7 +96,70 @@ async def add_security_headers(request: Request, call_next):
|
|||||||
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
|
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
|
||||||
|
|
||||||
# Content Security Policy
|
# Content Security Policy
|
||||||
response.headers["Content-Security-Policy"] = "default-src 'self'; frame-ancestors 'none'"
|
csp_mode = settings.CSP_MODE.lower()
|
||||||
|
|
||||||
|
# Special handling for API docs
|
||||||
|
is_docs = request.url.path in ["/docs", "/redoc"] or \
|
||||||
|
request.url.path.startswith("/docs/") or \
|
||||||
|
request.url.path.startswith("/redoc/")
|
||||||
|
|
||||||
|
if csp_mode == "disabled":
|
||||||
|
# No CSP (only for local development/debugging)
|
||||||
|
pass
|
||||||
|
elif is_docs:
|
||||||
|
# Always allow Swagger UI/ReDoc resources on docs pages
|
||||||
|
response.headers["Content-Security-Policy"] = (
|
||||||
|
"default-src 'self'; "
|
||||||
|
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
|
||||||
|
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com; "
|
||||||
|
"font-src 'self' https://fonts.gstatic.com; "
|
||||||
|
"img-src 'self' data: https://fastapi.tiangolo.com; "
|
||||||
|
"frame-ancestors 'none'"
|
||||||
|
)
|
||||||
|
elif csp_mode == "strict":
|
||||||
|
# Maximum security - blocks most external resources
|
||||||
|
# Use this in production if you don't need external CDNs
|
||||||
|
response.headers["Content-Security-Policy"] = (
|
||||||
|
"default-src 'self'; "
|
||||||
|
"script-src 'self'; "
|
||||||
|
"style-src 'self'; "
|
||||||
|
"img-src 'self' data:; "
|
||||||
|
"font-src 'self'; "
|
||||||
|
"connect-src 'self'; "
|
||||||
|
"frame-ancestors 'none'; "
|
||||||
|
"base-uri 'self'; "
|
||||||
|
"form-action 'self'"
|
||||||
|
)
|
||||||
|
else: # 'relaxed' mode (default)
|
||||||
|
# Modern frontend development - allows common CDNs and features
|
||||||
|
# Safe for most production apps that use external resources
|
||||||
|
response.headers["Content-Security-Policy"] = (
|
||||||
|
"default-src 'self'; "
|
||||||
|
# Allow common script sources
|
||||||
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval' "
|
||||||
|
"https://cdn.jsdelivr.net https://unpkg.com https://cdnjs.cloudflare.com "
|
||||||
|
"https://www.googletagmanager.com https://www.google-analytics.com; "
|
||||||
|
# Allow common style sources
|
||||||
|
"style-src 'self' 'unsafe-inline' "
|
||||||
|
"https://cdn.jsdelivr.net https://unpkg.com https://cdnjs.cloudflare.com "
|
||||||
|
"https://fonts.googleapis.com; "
|
||||||
|
# Allow common font sources
|
||||||
|
"font-src 'self' data: "
|
||||||
|
"https://fonts.gstatic.com https://cdn.jsdelivr.net; "
|
||||||
|
# Allow images from self, data URIs, and common CDNs
|
||||||
|
"img-src 'self' data: blob: https:; "
|
||||||
|
# Allow API calls to self and common services
|
||||||
|
"connect-src 'self' "
|
||||||
|
"https://www.google-analytics.com https://analytics.google.com; "
|
||||||
|
# Media from self and data URIs
|
||||||
|
"media-src 'self' data: blob:; "
|
||||||
|
# Prevent framing
|
||||||
|
"frame-ancestors 'none'; "
|
||||||
|
# Restrict base URI
|
||||||
|
"base-uri 'self'; "
|
||||||
|
# Restrict form submissions
|
||||||
|
"form-action 'self'"
|
||||||
|
)
|
||||||
|
|
||||||
# Permissions Policy (formerly Feature Policy)
|
# Permissions Policy (formerly Feature Policy)
|
||||||
response.headers["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()"
|
response.headers["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()"
|
||||||
|
|||||||
Reference in New Issue
Block a user