Repository navigation
fix: wildcard CORS + credentials spec violation, path-traversal guard on /icons proxy - #320
Conversation
… on /icons proxy
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 msoedov#298
Signed-off-by: Devam Shah <devamshah91@gmail.com>
jackspiece
left a comment
There was a problem hiding this comment.
I reproduced the 36 focused tests, plus the full suite at 402 passed, 5 skipped, and 41 deselected. The icon allowlist and containment check look sound. One CORS concern remains: dropping allow_credentials fixes the invalid header combination, but allow_origins=["*"] still permits browser requests to unauthenticated state-changing endpoints such as /verify, /scan, and /stop when network policy allows them. The bundled UI derives SELF_URL from window.location.href, so it appears to be same-origin. Is cross-origin access required elsewhere? If not, removing CORS or using an explicit configurable allowlist would close the more important browser-to-local-service surface. Otherwise, I suggest tracking that separately and documenting the expected cross-origin client.
|
@DevamShah @jackspiece Thank you for the patch and testing |
|
@jackspiece your CORS point was correct, and it turned out to be worse than I assumed when I scoped this PR to the header-combination bug. I went back and checked it against Confirming your reading first:
The part I had not appreciated: To your question — no, cross-origin access is not required anywhere I can find. So the answer is the explicit configurable allowlist you suggested, defaulting to empty rather than One thing worth adding to your framing: the allowlist alone does not finish the job. CORS governs whether the browser exposes the response, not whether the request is sent, and two of these endpoints are CORS simple requests that are never preflighted — I have filed that as #334 with the full write-up, and pushed both parts to I deliberately did not touch authentication. Adding auth to a single-operator localhost tool is a product decision for @msoedov, and the Origin guard closes the browser surface without it. |
Closes #298
Summary
Remove the
allow_credentials=True/allow_origins=["*"]spec-invalid combo and add a strict allowlist + path-containment check on the/icons/{icon_name}endpoint before any filesystem write or outbound fetch.Problem / motivation
Two interlocking issues were filed in #298:
1. CORS (middleware/cors.py:10)
allow_origins=["*"]combined withallow_credentials=Trueis explicitly forbidden by the CORS spec (Fetch §4.7, RFC 6454 §7.2). Browsers respond by silently stripping credentials on every cross-origin request, so the configuration neither works as intended nor provides any actual access control. The code reads as "any origin may make authenticated requests" but behaves as "any origin may make unauthenticated requests." Because the app uses Bearer tokens in request headers (not cookies),allow_credentialsis unnecessary here.2. Path traversal / arbitrary-file-write surface (routes/static.py:100)
icon_nameis taken directly from the URL path parameter and interpolated into both a filesystem path and an outbound URL with no validation. FastAPI's single-segment path parameters normally block literal/, but%2F-encoded slashes are handled inconsistently across Starlette versions and can decode to directory separators. The combined write surface represents CWE-22 (path traversal / arbitrary write) and a constrained SSRF-adjacent risk (controlled path component against a fixed host).Change
agentic_security/middleware/cors.pyallow_credentials=True. Wildcard origins remain; credentialed cross-origin access was not required and the setting was non-functional.agentic_security/routes/static.pyICON_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+\.png$").serve_icon()now validatesicon_nameagainstICON_NAME_REbefore any I/O; returns HTTP 400 on mismatch.is_relative_to(ICONS_DIR.resolve())as a defense-in-depth containment check; returns HTTP 400 if outside the icons directory. This layer defends against any future URL-handling change that could pass through encoded separators.tests/unit/test_cors_middleware.py(new)CORSMiddlewareis registered.allow_credentials=True.Access-Control-Allow-Credentials: trueis absent whenACAO: *.tests/integration/routes/test_static_icon_validation.py(new)TestIconNameRegex: parametrized unit tests for 7 valid and 11 invalid name patterns including path traversal, encoded slashes, wrong extensions, null bytes, and double extensions.TestServeIconValidation: integration tests viaTestClientconfirming 400 for invalid names, pass-through to 404 (not 400) for a valid-but-absent icon, and 200 for a cached icon (mocked FS).Security rationale
The regex allowlist follows the principle of positive validation (allowlist over denylist), which is the OWASP-recommended approach for filename inputs. The
is_relative_tocheck is a belt-and-suspenders layer that survives any future URL-decoding changes upstream.Testing / validation
Result: 36 passed (new CORS + icon-validation tests plus the existing
test_static.pysuite), no regressions.icon_nameis matched withre.fullmatchagainst an anchorless allowlist so a trailing newline (icon.png\n) is also rejected.Regex positive/negative coverage verified independently:
openai.pngclaude-3.png../etc/passwd../../secret.pngfoo%2Fbar.pngicon.PNGicon.png.sh.png(empty stem)