From bc9aa6624f45c1e0e6769d1356d25fca3d3994a5 Mon Sep 17 00:00:00 2001 From: twothatit Date: Thu, 19 Feb 2026 00:39:43 +0100 Subject: [PATCH] security: fix CORS wildcard, add security headers, enforce role check, sanitize errors - CORS: remove allow_origins=["*"]; restrict to ALLOWED_ORIGINS env var (comma-separated list); default is no cross-origin access. Removed allow_credentials=True and method/header wildcards. - Security headers middleware: add X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy, Strict-Transport-Security to all responses. - users.py: guard POST /api/users so only users with role="admin" can create new accounts (prevents privilege escalation by non-admin roles). - auth.py: remove raw exception detail from Azure AD 500 response to avoid leaking internal error messages / stack traces to clients. Co-Authored-By: Claude Sonnet 4.6 --- app/main.py | 31 ++++++++++++++++++++++++++----- app/routers/auth.py | 4 ++-- app/routers/users.py | 6 ++++++ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/app/main.py b/app/main.py index acf6e93..f644fe6 100644 --- a/app/main.py +++ b/app/main.py @@ -43,15 +43,36 @@ app = FastAPI( app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) -# CORS — allow same-origin; adjust if needed +# CORS — restrict to explicitly configured origins only. +# Set ALLOWED_ORIGINS in .env as a comma-separated list of allowed origins, +# e.g. ALLOWED_ORIGINS=https://myapp.example.com +# If unset, no cross-origin requests are allowed (same-origin only). +_raw_origins = os.environ.get("ALLOWED_ORIGINS", "") +_allowed_origins = [o.strip() for o in _raw_origins.split(",") if o.strip()] + app.add_middleware( CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], + allow_origins=_allowed_origins, + allow_credentials=False, + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allow_headers=["Authorization", "Content-Type"], ) +# --------------------------------------------------------------------------- +# Security headers middleware +# --------------------------------------------------------------------------- +@app.middleware("http") +async def add_security_headers(request: Request, call_next): + """Attach standard security headers to every response.""" + response = await call_next(request) + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["X-Frame-Options"] = "DENY" + response.headers["X-XSS-Protection"] = "1; mode=block" + response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" + response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" + return response + + # --------------------------------------------------------------------------- # Routers # --------------------------------------------------------------------------- diff --git a/app/routers/auth.py b/app/routers/auth.py index abd72b5..b02a48a 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -386,9 +386,9 @@ async def azure_callback( except HTTPException: raise - except Exception as exc: + except Exception: logger.exception("Azure AD authentication error") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Azure AD authentication error: {exc}", + detail="Azure AD authentication failed. Please try again or contact support.", ) diff --git a/app/routers/users.py b/app/routers/users.py index 6fafa69..0e8d9c9 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -33,6 +33,12 @@ async def create_user( db: Session = Depends(get_db), ): """Create a new local user.""" + if current_user.role != "admin": + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Only admins can create new users.", + ) + existing = db.query(User).filter(User.username == payload.username).first() if existing: raise HTTPException(