fix: skip default CSP on FastAPI docs paths (OHE-2815) - #118
Merged
Conversation
OHE-2815 Pen test remediation: Implement Content-Security-Policy (CSP) header
Source: All Hands Ai Web Application Penetration Test (June 2026), finding #1. Vanta: https://app.vanta.com/c/openhands.dev/tests/pen-test-remediation?tab=results Risk: Medium · OWASP A02 - Security Misconfiguration · CWE-693 Affected: https://staging.all-hands.dev/ (and presumably prod) Description: No Content-Security-Policy header is enforced. Leaves the app vulnerable to XSS and malicious content injection — no active XSS was found, but there's no defense-in-depth against it. Remediation:
|
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||
FastAPI's /docs (Swagger UI) and /redoc (ReDoc) serve HTML that
loads its CSS and JS from cdn.jsdelivr.net, which the default CSP
does not allowlist — so the browser blocks the docs page entirely
with a 'Loading the stylesheet … violates the following Content
Security Policy directive' error.
Rather than allowlist the whole CDN, exempt those paths from the
default CSP. The companion security headers (X-Content-Type-Options,
Referrer-Policy, Permissions-Policy, X-Frame-Options) are still
applied to the docs pages — only the CSP itself is skipped.
- Add _DEFAULT_CSP_SKIP_PATH_PREFIXES = ('/docs', '/redoc') and a
small _is_docs_path helper that anchors the prefix on a path
boundary (so e.g. /docs-v2 is not exempt).
- _policy() now returns (policy, is_override) so dispatch() can
distinguish a default policy (subject to the skip) from an
operator-set override (which still applies everywhere, including
the docs paths).
- The CONTENT_SECURITY_POLICY override and the empty-string kill
switch continue to work as before; the kill switch now also
covers the docs paths.
- Default policy string is unchanged — no new third-party origin
is added to script-src / style-src.
Tests cover: default policy skipped on /docs, /redoc and
/docs/oauth2-redirect; default policy still set on /openapi.json;
the prefix match anchors on a path boundary; the override still
applies to /docs; the kill switch still disables CSP on /docs.
Co-authored-by: openhands <openhands@all-hands.dev>
tofarr
force-pushed
the
openhands/d22d0f08-7442-4ccd-a943-54c4105d769b
branch
from
August 3, 2026 13:32
a56c218 to
d0faacf
Compare
tofarr
marked this pull request as ready for review
August 3, 2026 15:02
|
🚀 Released in 1.50.0. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
HUMAN:
AGENT:
Why
SecurityHeadersMiddleware(OHE-2815) sets an enforce-modeContent-Security-Policyon every response. FastAPI's default/docs(Swagger UI) and/redoc(ReDoc) pages serve HTML that loads their CSS and JS bundle fromhttps://cdn.jsdelivr.net/npm/swagger-ui-dist@5/, but that origin was not in the defaultstyle-src/script-src. The browser blocks the stylesheet, the page renders unstyled, and the bundle never runs. Error seen in the console:The previous approach considered for this fix (allowlisting the whole
cdn.jsdelivr.netorigin inscript-srcandstyle-src) was rejected: it would broaden the allowlist to any future jsdelivr-hosted asset, and the docs pages don't need a CSP at all since they don't execute any of our app code.Summary
_DEFAULT_CSP_SKIP_PATH_PREFIXES = ('/docs', '/redoc')and a small_is_docs_path()helper inopenhands/app_server/middleware.py. The helper anchors the prefix on a path boundary so e.g./docs-v2is not exempt._policy()now returns(policy, is_override)sodispatch()can distinguish a default policy (subject to the skip) from an operator-set override (which still applies everywhere, including the docs paths).dispatch()skips writingContent-Security-Policyfor docs paths when the default policy is in use. The companion security headers —X-Content-Type-Options,Referrer-Policy,Permissions-Policy,X-Frame-Options— are still set on the docs pages; only the CSP itself is omitted.CONTENT_SECURITY_POLICYoverride and theCONTENT_SECURITY_POLICY=""kill switch continue to work as before. The kill switch now also covers the docs paths.script-srcorstyle-src.Issue Number
OHE-2815 (the CSP middleware that introduced the regression).
How to Test
python3 -m pytest tests/unit/app_server/test_security_headers_middleware.py -v. All 22 tests should pass, including the 6 new ones:test_default_csp_skipped_on_docstest_default_csp_skipped_on_redoctest_default_csp_skipped_on_docs_subpathstest_default_csp_set_on_openapi_jsontest_path_that_only_starts_with_docs_letter_is_not_skippedtest_csp_override_still_applies_to_docstest_csp_kill_switch_still_works_on_docspython3 -m ruff checkandpython3 -m ruff format --checkon both modified files — both should be clean.curl -I https://<host>/docs— response should not includeContent-Security-Policy, but should includeX-Content-Type-Options,X-Frame-Options, etc.curl -I https://<host>/openapi.json— response should still include the defaultContent-Security-Policy./docsin a browser and confirm the Swagger UI renders without CSP violations in DevTools.CONTENT_SECURITY_POLICY="default-src 'none'"and re-hit/docs— the override should still apply, locking the docs down for operators who want that.Video/Screenshots
N/A — backend middleware change; the visible effect is that
/docsno longer shows a CSP violation in the browser console.Type
Notes
https://fastapi.tiangolo.com/img/favicon.pngwas already permitted by thehttps:wildcard inimg-src, so no change was needed there.https://cdn.jsdelivr.netinscript-src/style-src. Rejected because it broadens the allowlist to any future jsdelivr-hosted asset and the docs pages don't need a CSP at all (they serve only Swagger UI HTML, not our app code).https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/(narrower than a whole-origin allowlist but still leaves a CDN dependency), (b) Subresource Integrity hashes for the swagger-ui CSS/JS, (c) self-host the swagger-ui assets and serve them from'self'.Enterprise server image for this PR: