Skip to content

fix: skip default CSP on FastAPI docs paths (OHE-2815) - #118

Merged
tofarr merged 1 commit into
mainfrom
openhands/d22d0f08-7442-4ccd-a943-54c4105d769b
Aug 3, 2026
Merged

fix: skip default CSP on FastAPI docs paths (OHE-2815)#118
tofarr merged 1 commit into
mainfrom
openhands/d22d0f08-7442-4ccd-a943-54c4105d769b

Conversation

@tofarr

@tofarr tofarr commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

HUMAN:

  • A human has tested these changes.

AGENT:


Why

SecurityHeadersMiddleware (OHE-2815) sets an enforce-mode Content-Security-Policy on every response. FastAPI's default /docs (Swagger UI) and /redoc (ReDoc) pages serve HTML that loads their CSS and JS bundle from https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/, but that origin was not in the default style-src / script-src. The browser blocks the stylesheet, the page renders unstyled, and the bundle never runs. Error seen in the console:

Loading the stylesheet 'https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css' violates the following Content Security Policy directive: "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com". The action has been blocked.

The previous approach considered for this fix (allowlisting the whole cdn.jsdelivr.net origin in script-src and style-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

  • Add _DEFAULT_CSP_SKIP_PATH_PREFIXES = ('/docs', '/redoc') and a small _is_docs_path() helper in openhands/app_server/middleware.py. The helper 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).
  • dispatch() skips writing Content-Security-Policy for 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.
  • The CONTENT_SECURITY_POLICY override and the CONTENT_SECURITY_POLICY="" 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 or style-src.

Issue Number

OHE-2815 (the CSP middleware that introduced the regression).

How to Test

  1. Run the middleware unit tests: 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_docs
    • test_default_csp_skipped_on_redoc
    • test_default_csp_skipped_on_docs_subpaths
    • test_default_csp_set_on_openapi_json
    • test_path_that_only_starts_with_docs_letter_is_not_skipped
    • test_csp_override_still_applies_to_docs
    • test_csp_kill_switch_still_works_on_docs
  2. Run python3 -m ruff check and python3 -m ruff format --check on both modified files — both should be clean.
  3. End-to-end (deployed env):
    • curl -I https://<host>/docs — response should not include Content-Security-Policy, but should include X-Content-Type-Options, X-Frame-Options, etc.
    • curl -I https://<host>/openapi.json — response should still include the default Content-Security-Policy.
    • Load /docs in a browser and confirm the Swagger UI renders without CSP violations in DevTools.
    • Optional: 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 /docs no longer shows a CSP violation in the browser console.

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

  • The favicon at https://fastapi.tiangolo.com/img/favicon.png was already permitted by the https: wildcard in img-src, so no change was needed there.
  • Alternative fix considered (and rejected): allowlisting https://cdn.jsdelivr.net in script-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).
  • Tighter alternatives still on the table as follow-ups: (a) path-restrict to 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'.
  • No migration or rollout concerns: the default CSP is now narrower on the docs paths (less restrictive), and the override path is unchanged.
  • This PR was created by an AI agent (OpenHands) on behalf of the user; per the template, the human-tested checkbox above is intentionally left for a human reviewer to mark.

Enterprise server image for this PR:

ghcr.io/openhands/enterprise-server:sha-d0faacf

@linear

linear Bot commented Aug 3, 2026

Copy link
Copy Markdown
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:

  • Add a Content-Security-Policy header to all HTTP responses, starting with restrictive default-src, script-src, style-src directives (e.g. default-src 'self'; script-src 'self'; style-src 'self';)
  • Recommend rolling out in report-only mode first to catch violations without breaking functionality, then tighten and enforce
  • Review/update as the app evolves

Review in Linear

@github-actions github-actions Bot added the type: fix A bug fix label Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  openhands/app_server
  middleware.py 138, 149-158, 164-175, 183, 188
Project Total  

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
tofarr force-pushed the openhands/d22d0f08-7442-4ccd-a943-54c4105d769b branch from a56c218 to d0faacf Compare August 3, 2026 13:32
@tofarr tofarr changed the title fix: allow swagger UI assets in CSP (OHE-2815) fix: skip default CSP on FastAPI docs paths (OHE-2815) Aug 3, 2026
@tofarr
tofarr marked this pull request as ready for review August 3, 2026 15:02

@hieptl hieptl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! 🙏

@tofarr
tofarr merged commit debf2da into main Aug 3, 2026
22 checks passed
@tofarr
tofarr deleted the openhands/d22d0f08-7442-4ccd-a943-54c4105d769b branch August 3, 2026 15:05
@openhands-release-bot

Copy link
Copy Markdown

🚀 Released in 1.50.0.

@openhands-release-bot openhands-release-bot Bot added the released: 1.50.0 Shipped in 1.50.0 label Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

released: 1.50.0 Shipped in 1.50.0 type: fix A bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants