Add security response headers and CORS policy #47
Loading…
Reference in a new issue
No description provided.
Delete branch "refs/pull/47/head"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #45
Closes #46
Summary
SecurityHeadersMiddlewareinmiddleware.pysetsX-Frame-Options: DENY,X-Content-Type-Options: nosniff, andContent-Security-Policy: default-src 'self'on every response, blocking clickjacking and MIME-sniffing attacks.CORSMiddleware(Starlette built-in) registered withallow_origins=[]as the outermost middleware layer, making it explicit that this API does not support browser cross-origin access.Both are registered in
main.py. No new dependencies;CORSMiddlewareis already available transitively viapyview-web.Test plan
uv run --directory packages/fenliu tryke test— 626 tests pass (5 new intests/test_security_headers.py)uv run --directory packages/fenliu ruff check .— cleanuv run --directory packages/fenliu ty check .— cleanuv run --directory packages/fenliu complexipy .— cleanpackages/fenliu/src/fenliu/middleware.pyline 105 — @wuming-review🔴 Not actioned — the concern is based on an incorrect model of the middleware stack.
Starlette's
ExceptionMiddleware(which convertsHTTPExceptioninstances like 404, 401, 422 intoResponseobjects) sits insideSecurityHeadersMiddlewarein the stack:Because
ExceptionMiddlewareis inward ofSecurityHeadersMiddleware, itsResponseobjects flow back out throughSecurityHeadersMiddleware.dispatch, which adds the headers before returning to the client. This is confirmed by the test added in response to comment 17480261 — a 404 response carries all three security headers.For genuinely unhandled exceptions (non-
HTTPException),ServerErrorMiddleware(outermost, added automatically by Starlette) catches them and returns a generic 500 page that does not pass back throughSecurityHeadersMiddleware. In production mode this page contains no scripts or sensitive content, so the absence of CSP/X-Frame-Options on that edge case is an acceptable trade-off.packages/fenliu/tests/test_security_headers.pyline 132 — @wuming-review✅ Addressed — added
security_headers_present_on_404_responsein commitf0c7418. It hits/nonexistent-route-for-testing-404, asserts a 404 status, and verifies all three security headers are present. This also concretely demonstrates the claim in reply to 17480258 that HTTP error responses DO pass throughSecurityHeadersMiddleware.packages/fenliu/tests/test_security_headers.pyline 17 — @wuming-review🔴 Not actioned — this is the established, intentional pattern across every test file in the project (
test_middleware.py,test_main.py,test_startup.py, etc.). The value"test-secret-key-not-for-production"is self-describing. The three-guard block at the top of test files (settingDATABASE_URL,UI_AUTH_ENABLED, andSECRET_KEY) is a documented project convention. There is no path to production: the value is only set viaos.environ.setdefault, which leaves an existing env var untouched, and the production lifespan guard inmain.pyexplicitly rejects anySECRET_KEYthat matches a known placeholder pattern.packages/fenliu/Security-Audit-detail.mdline 352 — @wuming-review✅ Addressed — rephrased in commit
f0c7418to: "registered insrc/fenliu/main.pyas the second-outermost layer: outsideSessionMiddleware,APIKeyMiddleware, andUIAuthMiddleware, and insideCORSMiddleware(which is outermost)". Also added a sentence explaining thatExceptionMiddlewareis further inward, so HTTP error responses also receive the security headers.packages/fenliu/Security-Audit.mdline 18 — @wuming-review🔴 Not actioned — the annotation style is intentional.
✅on Critical and High means the section is fully resolved. Medium is partially resolved (M1 still open), so(M1 open)uses parentheses to signal an in-progress state rather than completion. Using✅would be misleading. The difference in symbol is a deliberate signal of status, not a typo.packages/fenliu/src/fenliu/main.pyline 154 — @wuming-review🔴 Not actioned —
allow_origins=[]is the correct and intentional choice, and it is exactly what the security audit (M3) recommends:FenLiu is a self-hosted backend API with no browser-based frontend client. The finding being fixed (M3) is precisely that the previous lack of
CORSMiddlewareleft cross-origin behaviour implicit (permissive by default).allow_origins=[]makes it explicit (deny by default). RemovingCORSMiddlewareentirely, as the reviewer suggests, would reintroduce the vulnerability.