Phase 9: production hardening
Config via env, abuse/resource limits, and production serving so the app is safe to expose publicly: - Fail-fast on missing SECRET_KEY when MULTI_USER=true - quickjs per-execution time/memory limits (while(true) can't hang server) - Per-user/per-IP rate limiting on turn/script/auth endpoints - Request body size limit + per-user row caps - Security headers (CSP, X-Frame-Options, nosniff, referrer-policy) incl. SSE - Debug router 403 and /docs disabled in multi-user mode - DATABASE_URL support (defaults to Neon Postgres) alongside SQLite - Documented all env vars in backend/.env.example Verified locally via uvicorn (MULTI_USER=1, SQLite); see plan/09-phase-hardening.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017e6tQuojBLYPetUfmhit4X
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
de4db373f2
commit
4772171b6c
+62
-2
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
@@ -5,21 +6,80 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
|
||||
from .auth import MULTI_USER
|
||||
from .database import engine
|
||||
from .limits import BodySizeLimitMiddleware
|
||||
from .migrations import bootstrap
|
||||
from .routers import adventures, auth, debug, scenarios, scripts, settings, story_cards
|
||||
|
||||
bootstrap(engine)
|
||||
|
||||
app = FastAPI(title="AI D&D")
|
||||
# Production serves the SPA same-origin, so CORS only matters for the Vite dev
|
||||
# server; AIDND_CORS_ORIGINS overrides for any other cross-origin setup.
|
||||
CORS_ORIGINS = [
|
||||
o.strip()
|
||||
for o in os.environ.get("AIDND_CORS_ORIGINS", "").split(",")
|
||||
if o.strip()
|
||||
] or ["http://localhost:5173", "http://127.0.0.1:5173"]
|
||||
|
||||
# The interactive API docs stay local-only: in multi-user mode they just hand
|
||||
# strangers a map of the API surface.
|
||||
app = FastAPI(
|
||||
title="AI D&D",
|
||||
docs_url=None if MULTI_USER else "/docs",
|
||||
redoc_url=None,
|
||||
openapi_url=None if MULTI_USER else "/openapi.json",
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
|
||||
allow_origins=CORS_ORIGINS,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.add_middleware(BodySizeLimitMiddleware)
|
||||
|
||||
|
||||
class SecurityHeadersMiddleware:
|
||||
"""Standard hardening headers on every response. Pure ASGI (wraps `send`)
|
||||
so SSE streams pass through unbuffered. The CSP allows exactly what the
|
||||
SPA uses: same-origin everything, inline styles (React), Google Fonts."""
|
||||
|
||||
_HEADERS = [
|
||||
(b"x-content-type-options", b"nosniff"),
|
||||
(b"referrer-policy", b"same-origin"),
|
||||
(b"x-frame-options", b"DENY"),
|
||||
(
|
||||
b"content-security-policy",
|
||||
b"default-src 'self'; "
|
||||
b"script-src 'self'; "
|
||||
b"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
|
||||
b"font-src https://fonts.gstatic.com; "
|
||||
b"img-src 'self' data:; "
|
||||
b"connect-src 'self'; "
|
||||
b"frame-ancestors 'none'",
|
||||
),
|
||||
]
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
if scope["type"] != "http":
|
||||
return await self.app(scope, receive, send)
|
||||
|
||||
async def send_with_headers(message):
|
||||
if message["type"] == "http.response.start":
|
||||
message.setdefault("headers", [])
|
||||
message["headers"] = list(message["headers"]) + self._HEADERS
|
||||
await send(message)
|
||||
|
||||
await self.app(scope, receive, send_with_headers)
|
||||
|
||||
|
||||
app.add_middleware(SecurityHeadersMiddleware)
|
||||
|
||||
app.include_router(auth.router)
|
||||
app.include_router(scenarios.router)
|
||||
app.include_router(adventures.router)
|
||||
|
||||
Reference in New Issue
Block a user