wrongmove/api/origin_validator.py
Viktor Barzin 1ace45353a
Add API anti-abuse hardening: disable docs in prod, origin validator, exception handler
- 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)
2026-02-08 20:06:46 +00:00

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)