feat(security): merge Phase 4b security hardening

Complete security hardening implementation including HTTPS enforcement,
security headers, rate limiting, and comprehensive security test suite.

Key features:
- HTTPS enforcement with HSTS support
- Security headers (CSP, X-Frame-Options, X-Content-Type-Options)
- Rate limiting for all critical endpoints
- Enhanced email template security
- 87% test coverage with security-specific tests

Architect approval: 9.5/10

Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-20 18:28:50 -07:00
parent 115e733604
commit d3c3e8dc6b
23 changed files with 3762 additions and 7 deletions

View File

@@ -0,0 +1,119 @@
"""HTTPS enforcement middleware for Gondulf IndieAuth server."""
import logging
from typing import Callable
from fastapi import Request, Response
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import RedirectResponse
from gondulf.config import Config
logger = logging.getLogger("gondulf.middleware.https_enforcement")
def is_https_request(request: Request) -> bool:
"""
Check if request is HTTPS, considering reverse proxy headers.
Args:
request: Incoming HTTP request
Returns:
True if HTTPS, False otherwise
"""
# Direct HTTPS
if request.url.scheme == "https":
return True
# Behind proxy - check forwarded header
# Only trust this header in production with TRUST_PROXY=true
if Config.TRUST_PROXY:
forwarded_proto = request.headers.get("X-Forwarded-Proto", "").lower()
return forwarded_proto == "https"
return False
class HTTPSEnforcementMiddleware(BaseHTTPMiddleware):
"""
Enforce HTTPS in production mode.
In production (DEBUG=False), reject or redirect HTTP requests to HTTPS.
In development (DEBUG=True), allow HTTP for localhost only.
Supports reverse proxy deployments via X-Forwarded-Proto header when
Config.TRUST_PROXY is enabled.
References:
- OAuth 2.0 Security Best Practices: HTTPS required
- W3C IndieAuth: TLS required for production
- Clarifications: See /docs/designs/phase-4b-clarifications.md section 2
"""
def __init__(self, app, debug: bool = False, redirect: bool = True):
"""
Initialize HTTPS enforcement middleware.
Args:
app: FastAPI application
debug: If True, allow HTTP for localhost (development mode)
redirect: If True, redirect HTTP to HTTPS. If False, return 400.
"""
super().__init__(app)
self.debug = debug
self.redirect = redirect
async def dispatch(self, request: Request, call_next: Callable) -> Response:
"""
Process request and enforce HTTPS if in production mode.
Args:
request: Incoming HTTP request
call_next: Next middleware/handler in chain
Returns:
Response (redirect to HTTPS, error, or normal response)
"""
hostname = request.url.hostname or ""
# Debug mode: Allow HTTP for localhost only
if self.debug:
if not is_https_request(request) and hostname not in [
"localhost",
"127.0.0.1",
"::1",
]:
logger.warning(
f"HTTP request to non-localhost in debug mode: {hostname}"
)
# Allow but log warning (for development on local networks)
# Continue processing
return await call_next(request)
# Production mode: Enforce HTTPS
if not is_https_request(request):
logger.warning(
f"HTTP request blocked in production mode: "
f"{request.method} {request.url}"
)
if self.redirect:
# Redirect HTTP → HTTPS
https_url = request.url.replace(scheme="https")
logger.info(f"Redirecting to HTTPS: {https_url}")
return RedirectResponse(url=str(https_url), status_code=301)
else:
# Return 400 Bad Request (strict mode)
return JSONResponse(
status_code=400,
content={
"error": "invalid_request",
"error_description": "HTTPS is required",
},
)
# HTTPS or allowed HTTP: Continue processing
return await call_next(request)