This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/src/dashboard/middleware/security_headers.py

144 lines
4.7 KiB
Python
Raw Normal View History

"""Security headers middleware for FastAPI.
Adds common security headers to all HTTP responses to improve
application security posture against various attacks.
"""
import logging
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
logger = logging.getLogger(__name__)
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
"""Middleware to add security headers to all responses.
Adds the following headers:
- X-Content-Type-Options: Prevents MIME type sniffing
- X-Frame-Options: Prevents clickjacking
- X-XSS-Protection: Enables browser XSS filter
- Referrer-Policy: Controls referrer information
- Permissions-Policy: Restricts feature access
- Content-Security-Policy: Mitigates XSS and data injection
- Strict-Transport-Security: Enforces HTTPS (production only)
Usage:
app.add_middleware(SecurityHeadersMiddleware)
# Or with production settings:
app.add_middleware(SecurityHeadersMiddleware, production=True)
Attributes:
production: If True, adds HSTS header for HTTPS enforcement.
csp_report_only: If True, sends CSP in report-only mode.
"""
def __init__(
self,
app,
production: bool = False,
csp_report_only: bool = False,
ruff (#169) * polish: streamline nav, extract inline styles, improve tablet UX - Restructure desktop nav from 8+ flat links + overflow dropdown into 5 grouped dropdowns (Core, Agents, Intel, System, More) matching the mobile menu structure to reduce decision fatigue - Extract all inline styles from mission_control.html and base.html notification elements into mission-control.css with semantic classes - Replace JS-built innerHTML with secure DOM construction in notification loader and chat history - Add CONNECTING state to connection indicator (amber) instead of showing OFFLINE before WebSocket connects - Add tablet breakpoint (1024px) with larger touch targets for Apple Pencil / stylus use and safe-area padding for iPad toolbar - Add active-link highlighting in desktop dropdown menus - Rename "Mission Control" page title to "System Overview" to disambiguate from the chat home page - Add "Home — Timmy Time" page title to index.html https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * fix(security): move auth-gate credentials to environment variables Hardcoded username, password, and HMAC secret in auth-gate.py replaced with os.environ lookups. Startup now refuses to run if any variable is unset. Added AUTH_GATE_SECRET/USER/PASS to .env.example. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * refactor(tooling): migrate from black+isort+bandit to ruff Replace three separate linting/formatting tools with a single ruff invocation. Updates tox.ini (lint, format, pre-push, pre-commit envs), .pre-commit-config.yaml, and CI workflow. Fixes all ruff errors including unused imports, missing raise-from, and undefined names. Ruff config maps existing bandit skips to equivalent S-rules. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 12:23:35 -04:00
custom_csp: str | None = None,
):
super().__init__(app)
self.production = production
self.csp_report_only = csp_report_only
# Build CSP directive
self.csp_directive = custom_csp or self._build_csp()
def _build_csp(self) -> str:
"""Build the Content-Security-Policy directive.
Creates a restrictive default policy that allows:
- Same-origin resources by default
- Inline scripts/styles (needed for HTMX/Bootstrap)
- Data URIs for images
- WebSocket connections
Returns:
CSP directive string.
"""
directives = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' cdn.jsdelivr.net", # HTMX needs inline
"style-src 'self' 'unsafe-inline' fonts.googleapis.com cdn.jsdelivr.net", # Bootstrap needs inline
"img-src 'self' data: blob:",
"font-src 'self' fonts.gstatic.com",
"connect-src 'self' ws: wss:", # WebSocket support
"media-src 'self'",
"object-src 'none'",
"frame-src 'none'",
"frame-ancestors 'self'",
"base-uri 'self'",
"form-action 'self'",
]
return "; ".join(directives)
def _add_security_headers(self, response: Response) -> None:
"""Add security headers to a response.
Args:
response: The response to add headers to.
"""
# Prevent MIME type sniffing
response.headers["X-Content-Type-Options"] = "nosniff"
# Prevent clickjacking
response.headers["X-Frame-Options"] = "SAMEORIGIN"
# Enable XSS protection (legacy browsers)
response.headers["X-XSS-Protection"] = "1; mode=block"
# Control referrer information
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
# Restrict browser features
response.headers["Permissions-Policy"] = (
"camera=(), "
"microphone=(), "
"geolocation=(), "
"payment=(), "
"usb=(), "
"magnetometer=(), "
"gyroscope=(), "
"accelerometer=()"
)
# Content Security Policy
csp_header = (
"Content-Security-Policy-Report-Only"
if self.csp_report_only
else "Content-Security-Policy"
)
response.headers[csp_header] = self.csp_directive
# HTTPS enforcement (production only)
if self.production:
response.headers["Strict-Transport-Security"] = (
"max-age=31536000; includeSubDomains; preload"
)
async def dispatch(self, request: Request, call_next) -> Response:
"""Add security headers to the response.
Args:
request: The incoming request.
call_next: Callable to get the response from downstream.
Returns:
Response with security headers added.
"""
try:
response = await call_next(request)
except Exception as exc:
logger.debug("Upstream error in security headers middleware: %s", exc)
from starlette.responses import PlainTextResponse
response = PlainTextResponse("Internal Server Error", status_code=500)
self._add_security_headers(response)
return response