Skip to content

feat(web): baseline browser security headers on HTTP responses#39

Merged
passcod merged 1 commit into
mainfrom
web-security-headers
Jun 17, 2026
Merged

feat(web): baseline browser security headers on HTTP responses#39
passcod merged 1 commit into
mainfrom
web-security-headers

Conversation

@passcod

@passcod passcod commented Jun 17, 2026

Copy link
Copy Markdown
Member

🤖 The ZAP baseline DAST scan failed on the plain-HTTP seedling-web surface because several browser security headers were missing. This sets them on every response and triages the two non-actionable findings.

Headers now set (crates/web/src/http.rs)

ZAP finding Header
10021 X-Content-Type-Options missing X-Content-Type-Options: nosniff
10020 Anti-clickjacking X-Frame-Options: DENY (+ CSP frame-ancestors 'none')
10038 CSP not set full Content-Security-Policy
10063 Permissions-Policy not set restrictive Permissions-Policy

The CSP is intentionally not maximally strict, because a naive policy would break the SPA:

  • style-src 'self' 'unsafe-inline' — MUI/Emotion and xterm inject runtime <style> blocks.
  • img-src/font-src 'self' data: — the favicon is a data: SVG.
  • connect-src 'self' https: — the SPA opens WebTransport to https://{wt_hostname}:{wt_port}/wt, a different and operator-configurable origin; 'self' would block it. A test locks this so it can't regress.

Triaged as IGNORE in .zap/rules.tsv

  • 10027 Suspicious Comments — false positive matching tokens in the minified frontend bundle.
  • 10049 Non-Storable Content — the SPA shell is served no-store by design (see spa.delivery).

HSTS (10035) stays Caddy-owned as before; the new content headers correctly are not.

Spec-first: adds transport.http.security-headers to docs/spec/web.md, annotated with impl and a verifying test.

Adds a CSP, X-Frame-Options, X-Content-Type-Options and Permissions-Policy
layer to every plain-HTTP response so the ZAP baseline scan passes. The CSP
keeps the SPA working: inline styles for MUI/Emotion/xterm, data: images and
fonts, and connect-src https: so WebTransport to the (different, configurable)
WT origin is permitted.

Triages the two non-actionable ZAP findings into .zap/rules.tsv: Suspicious
Comments (false positive in the minified bundle) and Non-Storable Content (the
SPA shell is no-store by design).
@passcod passcod merged commit 5ff55b1 into main Jun 17, 2026
9 of 10 checks passed
@passcod passcod deleted the web-security-headers branch June 17, 2026 15:29
passcod added a commit that referenced this pull request Jun 19, 2026
🤖 The nightly ZAP baseline surfaced a second tier of findings once the
initial security headers (#39) were in place — ZAP runs deeper passive
checks once a CSP exists. This resolves the real ones and triages the
rest. Verified by running the ZAP baseline locally against the stub
stack: **0 WARN-NEW**.

## Fixed in code

- **CSP: Wildcard Directive [10055]** — `connect-src` no longer uses the
`https:` scheme wildcard. It now names the exact WebTransport origin
(the request host on `wt_port`, reconstructed the same way
`auth::handle_connect` builds `wt_url`), so the header is built
per-request.
- **Cross-Origin-* headers [90004]** — added
`Cross-Origin-Embedder-Policy: require-corp`,
`Cross-Origin-Opener-Policy: same-origin`, and
`Cross-Origin-Resource-Policy: same-origin`. The console embeds no
third-party content, so full site isolation is safe.

## Triaged as IGNORE in `.zap/rules.tsv`

- **style-src unsafe-inline [10055]** — required by the Emotion/MUI
toolkit, which injects runtime inline styles and `style=` attributes.
Can't be dropped without server-side nonce templating of the embedded
SPA. The rest of the CSP stays tight in code and is guarded by a unit
test.
- **Timestamp Disclosure [10096]** and **Private IP Disclosure [2]** —
false positives in the minified bundle; the `10.0.0.1` is an example
placeholder in a host-entry form field (`Services.tsx`).
- **Modern Web Application [10109]** — informational SPA detection.

## Notes

`COEP: require-corp` changes resource-loading semantics in the browser;
the Playwright e2e suite exercises the real app and will catch any
regression. Spec item `transport.http.security-headers` updated
accordingly, with the unit test extended to assert the precise
`connect-src` origin and the isolation headers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant