- Disable OpenAPI docs/redoc/openapi.json when APP_ENV=production - Strip uvicorn Server header with --no-server-header in Dockerfile and docker-compose.yml - Add OriginValidatorMiddleware to reject state-changing requests from disallowed origins - Add global exception handler to prevent stack trace leakage on unhandled errors - Add tests for all new security features (OpenAPI, origin validation, exception handler, server header)
34 lines
1.2 KiB
Python
34 lines
1.2 KiB
Python
"""Origin validation middleware for state-changing requests."""
|
|
import logging
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.requests import Request
|
|
from starlette.responses import JSONResponse, Response
|
|
|
|
logger = logging.getLogger("uvicorn")
|
|
|
|
STATE_CHANGING_METHODS = {"POST", "PUT", "DELETE", "PATCH"}
|
|
|
|
|
|
class OriginValidatorMiddleware(BaseHTTPMiddleware):
|
|
"""Reject state-changing requests with mismatched Origin header."""
|
|
|
|
def __init__(self, app, allowed_origins: list[str] | None = None) -> None:
|
|
super().__init__(app)
|
|
self._allowed = {o.rstrip("/") for o in (allowed_origins or [])}
|
|
|
|
async def dispatch(self, request: Request, call_next) -> Response:
|
|
if request.method not in STATE_CHANGING_METHODS:
|
|
return await call_next(request)
|
|
|
|
origin = request.headers.get("origin")
|
|
if origin is None:
|
|
return await call_next(request)
|
|
|
|
if origin.rstrip("/") not in self._allowed:
|
|
logger.warning(f"Rejected request from origin: {origin}")
|
|
return JSONResponse(
|
|
status_code=403,
|
|
content={"detail": "Origin not allowed"},
|
|
)
|
|
|
|
return await call_next(request)
|