Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .audit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ policy:

# Documented exceptions with business justification
# Format: CVE-XXXX-XXXXX or GHSA-xxxx-xxxx-xxxx
exceptions: {}
exceptions:
GHSA-gv7w-rqvm-qjhr:
package: esbuild
reason: "esbuild vulnerability affects Deno module only; SecuScan uses esbuild via Vite for bundling in Node.js context. Fix requires Vite 8.x breaking upgrade."
expires_at: "2026-08-31"

# Packages to exclude from audits (use sparingly!)
excluded_packages: []
Expand Down
3 changes: 2 additions & 1 deletion backend/secuscan/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import shutil
from pathlib import Path
from contextlib import asynccontextmanager
from .request_middleware import RequestIDMiddleware
from .request_middleware import RequestIDMiddleware, HardenCORSMiddleware

from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, PlainTextResponse
Expand Down Expand Up @@ -170,6 +170,7 @@ async def redirect_api_openapi():
allow_methods=settings.cors_allowed_methods,
allow_headers=settings.cors_allowed_headers,
)
app.add_middleware(HardenCORSMiddleware)
app.add_middleware(RequestIDMiddleware)

@app.exception_handler(StarletteHTTPException)
Expand Down
35 changes: 34 additions & 1 deletion backend/secuscan/request_middleware.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
from fastapi import Request
import logging
from .request_context import set_request_id
from .config import settings

logger = logging.getLogger(__name__)


class RequestIDMiddleware(BaseHTTPMiddleware):
Expand All @@ -18,4 +23,32 @@ async def dispatch(self, request: Request, call_next):
# Return ID in response headers
response.headers["X-Request-ID"] = request_id

return response
return response


class HardenCORSMiddleware(BaseHTTPMiddleware):
"""
Harden CORS by validating Origin headers against the allowed origins
when present. Requests without an Origin header are direct API calls
(not CORS) and are allowed through.
"""
async def dispatch(self, request: Request, call_next):
origin = request.headers.get("origin")
if not origin:
return await call_next(request)

if request.url.path in ["/docs", "/redoc", "/openapi.json", "/"]:
return await call_next(request)

allowed = settings.cors_allowed_origins
if origin in allowed or "*" in allowed:
return await call_next(request)

logger.warning(f"⚠️ [CORS] Rejected cross-origin request from {origin}")
return JSONResponse(
content={
"success": False,
"message": f"Not allowed by CORS: Origin '{origin}' is not permitted"
},
status_code=403
)
20 changes: 20 additions & 0 deletions testing/backend/integration/test_cors.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,23 @@ def test_cors_preflight_allows_preview_origin(test_client):

assert response.status_code == 200
assert response.headers.get("access-control-allow-origin") == origin

def test_cors_rejects_disallowed_origin(test_client):
response = test_client.get("/api/v1/health", headers={"Origin": "https://evil.com"})

assert response.status_code == 403
assert response.json()["success"] is False
assert "not permitted" in response.json()["message"]


def test_cors_allows_no_origin(test_client):
response = test_client.get("/api/v1/health")
assert response.status_code == 200


def test_cors_allows_docs_without_origin(test_client):
# Documentation endpoints should still be accessible without Origin header
# (e.g. by directly opening in browser)
for path in ["/docs", "/redoc", "/openapi.json"]:
response = test_client.get(path)
assert response.status_code == 200
Loading