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 <noreply@anthropic.com>
This commit is contained in:
31
app/main.py
31
app/main.py
@@ -43,15 +43,36 @@ app = FastAPI(
|
|||||||
app.state.limiter = limiter
|
app.state.limiter = limiter
|
||||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
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(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=_allowed_origins,
|
||||||
allow_credentials=True,
|
allow_credentials=False,
|
||||||
allow_methods=["*"],
|
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
allow_headers=["*"],
|
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
|
# Routers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -386,9 +386,9 @@ async def azure_callback(
|
|||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as exc:
|
except Exception:
|
||||||
logger.exception("Azure AD authentication error")
|
logger.exception("Azure AD authentication error")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
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.",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ async def create_user(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Create a new local user."""
|
"""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()
|
existing = db.query(User).filter(User.username == payload.username).first()
|
||||||
if existing:
|
if existing:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
Reference in New Issue
Block a user