feat(security): add configurable security headers middleware
- Add SecurityHeadersMiddleware with HSTS, CSP, X-Frame-Options, X-Content-Type-Options - Add configuration options in app/config.py - Integrate middleware into app/main.py - Add comprehensive tests in tests/test_security_headers.py - Update .env.demo with security header examples - Update docs/DeploymentGuide.md with security headers section and Traefik/Nginx examples - Update docs/ConfigurationGuide.md with detailed configuration reference - Update SECURITY_AUDIT.md to mark security headers implementation complete Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -174,6 +174,43 @@ class Settings(BaseSettings):
|
||||
description="Maximum size for a single file chunk in bytes. If set and file exceeds this, it will be split into smaller chunks for processing. Default: None (no splitting).",
|
||||
)
|
||||
|
||||
# Security Headers Configuration (see SECURITY_AUDIT.md and docs/DeploymentGuide.md)
|
||||
# When deploying behind a reverse proxy (Traefik, Nginx, etc.), disable these headers
|
||||
# if your proxy already adds them to avoid duplication
|
||||
security_headers_enabled: bool = Field(
|
||||
default=True,
|
||||
description="Enable security headers middleware. Set to False if reverse proxy handles headers.",
|
||||
)
|
||||
|
||||
# Strict-Transport-Security (HSTS) - Forces HTTPS connections
|
||||
security_header_hsts_enabled: bool = Field(
|
||||
default=True, description="Enable HSTS header. Only effective over HTTPS."
|
||||
)
|
||||
security_header_hsts_value: str = Field(
|
||||
default="max-age=31536000; includeSubDomains",
|
||||
description="HSTS header value. Default: 1 year with subdomains.",
|
||||
)
|
||||
|
||||
# Content-Security-Policy (CSP) - Controls resource loading
|
||||
security_header_csp_enabled: bool = Field(default=True, description="Enable CSP header.")
|
||||
security_header_csp_value: str = Field(
|
||||
default="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;",
|
||||
description="CSP header value. Customize based on your application's resource loading needs.",
|
||||
)
|
||||
|
||||
# X-Frame-Options - Prevents clickjacking
|
||||
security_header_x_frame_options_enabled: bool = Field(
|
||||
default=True, description="Enable X-Frame-Options header."
|
||||
)
|
||||
security_header_x_frame_options_value: str = Field(
|
||||
default="DENY", description="X-Frame-Options header value. Options: DENY, SAMEORIGIN, or ALLOW-FROM uri"
|
||||
)
|
||||
|
||||
# X-Content-Type-Options - Prevents MIME sniffing
|
||||
security_header_x_content_type_options_enabled: bool = Field(
|
||||
default=True, description="Enable X-Content-Type-Options header (always set to 'nosniff')."
|
||||
)
|
||||
|
||||
@validator("notification_urls", pre=True)
|
||||
def parse_notification_urls(cls, v):
|
||||
"""Parse notification URLs from string or list"""
|
||||
|
||||
+12
-3
@@ -17,6 +17,7 @@ from app.api import router as api_router
|
||||
from app.auth import router as auth_router
|
||||
from app.config import settings
|
||||
from app.database import init_db
|
||||
from app.middleware.security_headers import SecurityHeadersMiddleware
|
||||
from app.utils.config_validator import check_all_configs
|
||||
from app.utils.notification import init_apprise, notify_shutdown, notify_startup
|
||||
|
||||
@@ -99,13 +100,21 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
app = FastAPI(title="DocuElevate", lifespan=lifespan)
|
||||
|
||||
# 1) Session Middleware (for request.session to work)
|
||||
# Middleware stack (order matters - applied in reverse order)
|
||||
# Last added middleware is executed first
|
||||
|
||||
# 1) Security Headers Middleware (outermost - adds headers to final response)
|
||||
# Configure via SECURITY_HEADERS_ENABLED environment variable
|
||||
# Set to False if reverse proxy (Traefik, Nginx) handles security headers
|
||||
app.add_middleware(SecurityHeadersMiddleware, config=settings)
|
||||
|
||||
# 2) Session Middleware (for request.session to work)
|
||||
app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET)
|
||||
|
||||
# 2) Respect the X-Forwarded-* headers from Traefik
|
||||
# 3) Respect the X-Forwarded-* headers from reverse proxy (Traefik, Nginx)
|
||||
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*")
|
||||
|
||||
# 3) (Optional but recommended) Restrict valid hosts:
|
||||
# 4) Restrict valid hosts to prevent Host header attacks
|
||||
app.add_middleware(TrustedHostMiddleware, allowed_hosts=[settings.external_hostname, "localhost", "127.0.0.1"])
|
||||
|
||||
# Mount the static files directory
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Middleware package for DocuElevate."""
|
||||
|
||||
from app.middleware.security_headers import SecurityHeadersMiddleware
|
||||
|
||||
__all__ = ["SecurityHeadersMiddleware"]
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Security Headers Middleware for DocuElevate.
|
||||
|
||||
This middleware adds security headers to HTTP responses to improve browser-side security.
|
||||
Headers can be configured via environment variables to support different deployment scenarios:
|
||||
- Direct deployment: Enable all security headers
|
||||
- Reverse proxy deployment (Traefik, Nginx, etc.): Disable headers if proxy adds them
|
||||
|
||||
See docs/DeploymentGuide.md for configuration guidance.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from fastapi import Request, Response
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Middleware to add security headers to HTTP responses.
|
||||
|
||||
This middleware adds the following security headers when enabled:
|
||||
- Strict-Transport-Security (HSTS): Forces HTTPS connections
|
||||
- Content-Security-Policy (CSP): Controls resource loading
|
||||
- X-Frame-Options: Prevents clickjacking attacks
|
||||
- X-Content-Type-Options: Prevents MIME-sniffing attacks
|
||||
|
||||
Headers are configurable via environment variables to support different deployment scenarios.
|
||||
"""
|
||||
|
||||
def __init__(self, app, config):
|
||||
"""
|
||||
Initialize the security headers middleware.
|
||||
|
||||
Args:
|
||||
app: FastAPI application instance
|
||||
config: Configuration object with security header settings
|
||||
"""
|
||||
super().__init__(app)
|
||||
self.config = config
|
||||
self.enabled = config.security_headers_enabled
|
||||
|
||||
if self.enabled:
|
||||
logger.info("Security headers middleware enabled")
|
||||
logger.debug(
|
||||
f"HSTS: {config.security_header_hsts_enabled}, "
|
||||
f"CSP: {config.security_header_csp_enabled}, "
|
||||
f"X-Frame-Options: {config.security_header_x_frame_options_enabled}, "
|
||||
f"X-Content-Type-Options: {config.security_header_x_content_type_options_enabled}"
|
||||
)
|
||||
else:
|
||||
logger.info("Security headers middleware disabled (likely handled by reverse proxy)")
|
||||
|
||||
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
||||
"""
|
||||
Process the request and add security headers to the response.
|
||||
|
||||
Args:
|
||||
request: Incoming HTTP request
|
||||
call_next: Next middleware or route handler
|
||||
|
||||
Returns:
|
||||
HTTP response with security headers added (if enabled)
|
||||
"""
|
||||
# Process the request
|
||||
response = await call_next(request)
|
||||
|
||||
# Add security headers if enabled
|
||||
if self.enabled:
|
||||
self._add_security_headers(response)
|
||||
|
||||
return response
|
||||
|
||||
def _add_security_headers(self, response: Response) -> None:
|
||||
"""
|
||||
Add configured security headers to the response.
|
||||
|
||||
Args:
|
||||
response: HTTP response to add headers to
|
||||
"""
|
||||
# Strict-Transport-Security (HSTS)
|
||||
# Forces browsers to use HTTPS for all future requests to this domain
|
||||
# max-age: Time in seconds browsers should remember to only use HTTPS
|
||||
# includeSubDomains: Apply to all subdomains
|
||||
# preload: Allow inclusion in browser HSTS preload lists
|
||||
if self.config.security_header_hsts_enabled:
|
||||
hsts_value = self.config.security_header_hsts_value
|
||||
response.headers["Strict-Transport-Security"] = hsts_value
|
||||
logger.debug(f"Added HSTS header: {hsts_value}")
|
||||
|
||||
# Content-Security-Policy (CSP)
|
||||
# Controls which resources browsers are allowed to load for this page
|
||||
# This helps prevent XSS attacks and other code injection attacks
|
||||
if self.config.security_header_csp_enabled:
|
||||
csp_value = self.config.security_header_csp_value
|
||||
response.headers["Content-Security-Policy"] = csp_value
|
||||
logger.debug(f"Added CSP header: {csp_value[:50]}...")
|
||||
|
||||
# X-Frame-Options
|
||||
# Prevents the page from being loaded in a frame/iframe
|
||||
# This helps prevent clickjacking attacks
|
||||
if self.config.security_header_x_frame_options_enabled:
|
||||
x_frame_value = self.config.security_header_x_frame_options_value
|
||||
response.headers["X-Frame-Options"] = x_frame_value
|
||||
logger.debug(f"Added X-Frame-Options header: {x_frame_value}")
|
||||
|
||||
# X-Content-Type-Options
|
||||
# Prevents browsers from MIME-sniffing responses away from declared content-type
|
||||
# This helps prevent XSS attacks based on content-type confusion
|
||||
if self.config.security_header_x_content_type_options_enabled:
|
||||
response.headers["X-Content-Type-Options"] = "nosniff"
|
||||
logger.debug("Added X-Content-Type-Options header: nosniff")
|
||||
Reference in New Issue
Block a user