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
+9
-21
@@ -16,8 +16,6 @@ whitelist and a per-day turn cap.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
from collections import defaultdict, deque
|
||||
from dataclasses import dataclass
|
||||
from datetime import timezone
|
||||
|
||||
@@ -35,7 +33,15 @@ def _env_flag(name: str) -> bool:
|
||||
MULTI_USER = _env_flag("AIDND_MULTI_USER")
|
||||
|
||||
SESSION_COOKIE = "aidnd_session"
|
||||
COOKIE_SECURE = _env_flag("AIDND_COOKIE_SECURE") # enable behind HTTPS in prod
|
||||
# Secure cookies default on in multi-user (hosted = HTTPS; browsers also
|
||||
# accept Secure on http://localhost). AIDND_COOKIE_SECURE=0/1 overrides —
|
||||
# e.g. 0 when testing multi-user over plain http on a LAN address.
|
||||
_cookie_secure_env = os.environ.get("AIDND_COOKIE_SECURE", "").strip().lower()
|
||||
COOKIE_SECURE = (
|
||||
_cookie_secure_env in ("1", "true", "yes", "on")
|
||||
if _cookie_secure_env
|
||||
else MULTI_USER
|
||||
)
|
||||
COOKIE_MAX_AGE = 60 * 60 * 24 * 365
|
||||
|
||||
# ---------- Shared demo key (BYOK fallback) ----------
|
||||
@@ -151,21 +157,3 @@ def get_current_user(request: Request, db: Session = Depends(get_db)) -> models.
|
||||
raise HTTPException(401, "No session. Call GET /api/auth/me first.")
|
||||
_touch(user, db)
|
||||
return user
|
||||
|
||||
|
||||
# ---------- Brute-force limiter for register/login ----------
|
||||
|
||||
_ATTEMPT_LIMIT = 10
|
||||
_ATTEMPT_WINDOW = 300 # seconds
|
||||
_attempts: dict[str, deque] = defaultdict(deque)
|
||||
|
||||
|
||||
def rate_limit_auth(request: Request) -> None:
|
||||
ip = request.client.host if request.client else "unknown"
|
||||
now = time.time()
|
||||
window = _attempts[ip]
|
||||
while window and window[0] < now - _ATTEMPT_WINDOW:
|
||||
window.popleft()
|
||||
if len(window) >= _ATTEMPT_LIMIT:
|
||||
raise HTTPException(429, "Too many attempts. Try again in a few minutes.")
|
||||
window.append(now)
|
||||
|
||||
Reference in New Issue
Block a user