mirror of
https://github.com/msoedov/agentic_security.git
synced 2026-08-08 10:48:37 +02:00
CORS: drop allow_credentials=True. With allow_origins=['*'] Starlette reflects
the request Origin and emits Access-Control-Allow-Credentials: true on any
credentialed request — a reflect-any-origin hole. The app authenticates with
Bearer tokens in the Authorization header, so credentialed CORS was never needed.
/icons/{icon_name}: validate against a strict allowlist (re.fullmatch
[A-Za-z0-9._-]+\.png) and assert the resolved path stays inside ICONS_DIR before
any filesystem write or outbound fetch. Closes the path-traversal / arbitrary-write
(CWE-22) and controlled-URL (CWE-73) surface on the unsanitized icon_name.
Adds CORS middleware tests and icon-name validation tests (traversal, encoded
slash, null byte, trailing newline, wrong extension).
Closes #298
Signed-off-by: Devam Shah <devamshah91@gmail.com>
95 lines
3.4 KiB
Python
95 lines
3.4 KiB
Python
"""Unit tests for CORS middleware configuration.
|
|
|
|
Verifies that the wildcard-origins + allow_credentials=True spec violation
|
|
(CORS spec §3.2, Fetch §4.7) has been removed. Browsers silently strip
|
|
credentials when the response carries Access-Control-Allow-Origin: * paired
|
|
with Access-Control-Allow-Credentials: true, so the old config was both
|
|
broken and misleading.
|
|
"""
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.testclient import TestClient
|
|
|
|
from agentic_security.middleware.cors import setup_cors
|
|
|
|
|
|
def _get_cors_options(app: FastAPI) -> dict:
|
|
"""Extract CORS middleware options from the app's middleware stack."""
|
|
for middleware in app.user_middleware:
|
|
if middleware.cls is CORSMiddleware:
|
|
return middleware.kwargs
|
|
return {}
|
|
|
|
|
|
class TestCorsSetup:
|
|
"""CORS middleware is configured correctly."""
|
|
|
|
def test_cors_middleware_is_registered(self):
|
|
"""setup_cors adds CORSMiddleware to the app."""
|
|
app = FastAPI()
|
|
setup_cors(app)
|
|
cls_names = [m.cls.__name__ for m in app.user_middleware]
|
|
assert "CORSMiddleware" in cls_names
|
|
|
|
def test_wildcard_origins_without_credentials(self):
|
|
"""allow_origins=['*'] must not be paired with allow_credentials=True.
|
|
|
|
The combination is forbidden by the CORS spec and causes browsers to
|
|
silently drop credentials on every cross-origin request.
|
|
"""
|
|
app = FastAPI()
|
|
setup_cors(app)
|
|
opts = _get_cors_options(app)
|
|
allow_origins = opts.get("allow_origins", [])
|
|
allow_credentials = opts.get("allow_credentials", False)
|
|
|
|
if "*" in allow_origins or allow_origins == ["*"]:
|
|
assert not allow_credentials, (
|
|
"allow_origins=['*'] with allow_credentials=True is invalid per "
|
|
"the CORS spec — browsers reject it and credentials are silently dropped"
|
|
)
|
|
|
|
def test_cors_allows_cross_origin_requests(self):
|
|
"""Cross-origin preflight requests return a 200 with CORS headers."""
|
|
app = FastAPI()
|
|
|
|
@app.get("/probe")
|
|
async def probe():
|
|
return {"ok": True}
|
|
|
|
setup_cors(app)
|
|
client = TestClient(app, raise_server_exceptions=True)
|
|
|
|
response = client.options(
|
|
"/probe",
|
|
headers={
|
|
"Origin": "http://localhost:3000",
|
|
"Access-Control-Request-Method": "GET",
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
assert "access-control-allow-origin" in response.headers
|
|
|
|
def test_cors_no_credentials_header_with_wildcard(self):
|
|
"""With wildcard origins, the response must not include
|
|
Access-Control-Allow-Credentials: true."""
|
|
app = FastAPI()
|
|
|
|
@app.get("/probe")
|
|
async def probe():
|
|
return {"ok": True}
|
|
|
|
setup_cors(app)
|
|
client = TestClient(app)
|
|
response = client.get("/probe", headers={"Origin": "http://evil.example.com"})
|
|
|
|
acao = response.headers.get("access-control-allow-origin", "")
|
|
acac = response.headers.get("access-control-allow-credentials", "false")
|
|
|
|
if acao == "*":
|
|
assert acac.lower() != "true", (
|
|
"Wildcard ACAO + ACAC:true is a spec violation (RFC 6454 §7.2, "
|
|
"Fetch §4.7) and silently breaks credentialed cross-origin requests"
|
|
)
|