From 45fe6548eb9c7b0ae2d871e91d9bccb0a0eb6f03 Mon Sep 17 00:00:00 2001 From: Walid Ladeb Date: Wed, 18 Mar 2026 04:24:04 +0100 Subject: [PATCH] fix: secure CORS configuration (P0) --- backend/.env.example | 5 +++++ backend/main.py | 30 +++++++++++++++++++++++++++++- docker/docker-compose.yml | 2 ++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/backend/.env.example b/backend/.env.example index d128720..f58834a 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -9,6 +9,11 @@ DATABASE_URL=postgresql+asyncpg://aaip:aaip_secret@db:5432/aaip # Set to "true" to skip auth in local development (never in production) AAIP_DEV_MODE=false +# ── CORS Security ──────────────────────────── +# Environment: development or production +ENV=development +# Comma-separated list of allowed origins (no wildcards in production) +AAIP_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8000 # ── AI Judges (via OpenRouter) ─────────────── # Required for multi-model jury evaluation # Get your key at https://openrouter.ai diff --git a/backend/main.py b/backend/main.py index de8c30f..439cfd9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -11,6 +11,8 @@ """ from __future__ import annotations +import logging +import os from contextlib import asynccontextmanager from datetime import datetime from typing import AsyncIterator @@ -64,9 +66,35 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: redoc_url="/redoc", ) +# ── CORS Configuration ──────────────────────────────────────────────────────── +logger = logging.getLogger(__name__) + +# Safe ENV handling +env = os.getenv("ENV", "development").strip().lower() + +# Parse AAIP_ALLOWED_ORIGINS +origins = os.getenv("AAIP_ALLOWED_ORIGINS", "") +origin_list = [o.strip() for o in origins.split(",") if o.strip()] + +# Development defaults +if not origin_list and env != "production": + origin_list = ["http://localhost:3000", "http://localhost:8000"] + +# Empty origins protection +if not origin_list: + raise ValueError("CORS configuration error: AAIP_ALLOWED_ORIGINS is empty or invalid") + +# Production validation +if env == "production": + if "*" in origin_list: + raise ValueError("Wildcard '*' not allowed in production CORS configuration") + +# Log only count (privacy) +logger.info(f"CORS configured with {len(origin_list)} allowed origin(s)") + app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=origin_list, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 7ee9afb..d640448 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -40,6 +40,8 @@ services: CELERY_RESULT_BACKEND: redis://redis:6379/1 REDIS_URL: redis://redis:6379/0 AAIP_DEV_MODE: "false" + ENV: development + AAIP_ALLOWED_ORIGINS: http://localhost:3000,http://localhost:8000 depends_on: db: condition: service_healthy