From 31fc1e44c8aad6279e1daaab9d44b4e4be8337d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Apr 2026 08:46:24 +0000 Subject: [PATCH] fix(web): add Content-Security-Policy meta tag to index.html (#175) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lock down the document head with a CSP that matches the app's actual loads: * default-src 'self' / object-src 'none' / frame-ancestors 'none' / base-uri 'self' / form-action 'self' — kill the broad injection + clickjacking surfaces. * script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval' — 'wasm-unsafe-eval' is required for the Leptos WASM module; 'unsafe-eval' is currently required because crates/web still calls js_sys::eval() (theme bootstrap, focus helpers, relay-URL probe). Removing it is tracked by issues #171 / #425. Documented inline so the next person who reads the head sees the rationale. * style-src 'self' 'unsafe-inline' + Google Fonts host — Leptos views emit inline style="…" attributes, and foundation.css @imports the Fraunces / IBM Plex Sans / JetBrains Mono stylesheet. font-src separately allow-lists fonts.gstatic.com for the actual font files. * connect-src 'self' ws: wss: https: — relay WebSocket transport plus the relay's /bootstrap-id HTTP probe. * img-src 'self' data: blob: + media-src 'self' blob: — avatar data URIs, runtime URL.createObjectURL attachments, the chime.webm. * worker-src 'self' — service worker registration in init.js. Add a static-asset test that asserts the meta tag is present and that each required directive substring is in the content attribute, so any future loosening / rewording trips the test before regressing the policy. Refs #175 --- crates/web/index.html | 24 ++++++++++++++++ crates/web/tests/static_assets.rs | 47 +++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/crates/web/index.html b/crates/web/index.html index 11e3f8b2..11506818 100644 --- a/crates/web/index.html +++ b/crates/web/index.html @@ -2,6 +2,30 @@ + + diff --git a/crates/web/tests/static_assets.rs b/crates/web/tests/static_assets.rs index 0343c5e0..593d9af3 100644 --- a/crates/web/tests/static_assets.rs +++ b/crates/web/tests/static_assets.rs @@ -112,6 +112,53 @@ fn index_html_only_references_bundled_svg_icons() { assert_all_in_allow_list("index.html", &refs); } +/// Required directives for the CSP meta tag baked into `index.html`. +/// +/// Each entry is a substring search against the meta tag's `content` value; +/// they collectively encode the policy decisions described in the inline +/// HTML comment beside the tag (see GitHub issue #175). If you intentionally +/// loosen or tighten one of these directives, update both this list and the +/// inline comment so the rationale stays in sync. +const REQUIRED_CSP_DIRECTIVES: &[&str] = &[ + "default-src 'self'", + // WASM module + the still-extant js_sys::eval() sites (tracked by + // issues #171 / #425). Drop 'unsafe-eval' once those are gone. + "script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval'", + // Inline style="…" attrs from Leptos views + Google Fonts CSS @import. + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", + "font-src 'self' https://fonts.gstatic.com", + // ws/wss for relay transport, https for the relay HTTP bootstrap probe. + "connect-src 'self' ws: wss: https:", + // data: for avatar URIs, blob: for runtime createObjectURL attachments. + "img-src 'self' data: blob:", + "media-src 'self' blob:", + "worker-src 'self'", + "object-src 'none'", + "base-uri 'self'", + "form-action 'self'", + "frame-ancestors 'none'", +]; + +#[test] +fn index_html_declares_content_security_policy() { + let contents = read_asset("index.html"); + let needle = "http-equiv=\"Content-Security-Policy\""; + assert!( + contents.contains(needle), + "index.html is missing the Content-Security-Policy meta tag. \ + See GitHub issue #175 — the CSP guards against script injection \ + and clickjacking and must stay in the document head.", + ); + for directive in REQUIRED_CSP_DIRECTIVES { + assert!( + contents.contains(directive), + "index.html CSP is missing required directive `{directive}`. \ + Update both the meta tag in index.html and the rationale \ + comment beside it if you are intentionally changing this.", + ); + } +} + #[test] fn manifest_json_only_references_bundled_svg_icons() { let contents = read_asset("manifest.json");