fix(web): self-host CDN scripts and drop script-src 'unsafe-inline' #422

Merged
Claude-Run merged 1 commit from fix/BUNYIP-424-self-host-scripts-drop-unsafe-inline into main 2026-07-31 18:40:32 +02:00
Member

bunyip-web loaded htmx from unpkg.com and Font Awesome from its kit loader at kit.fontawesome.com, neither with an integrity attribute, and its CSP carried script-src 'self' 'unsafe-inline' https://unpkg.com https://kit.fontawesome.com. That was a standing remote-code-execution grant on the origin that holds the session: the kit rotates its own contents by design and cannot carry SRI, unpkg resolves htmx from npm at request time, and 'unsafe-inline' meant CSP was no barrier to any future reflected-XSS bug in the SSR pages either. The cookies are httpOnly, but injected JS does not need to read them - it issues same-origin fetches the browser attaches them to.

Both libraries are now vendored under bunyip-web/assets/vendor/ with the version in the path so an upgrade is a visible diff: htmx 2.0.3 (byte-identical to the published dist, sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq) and the Font Awesome 6.7.2 Free webfont build (core + solid + regular CSS and their woff2/ttf faces) replacing the kit loader.

Every inline <script> body and every on*= handler moved into bunyip-web/assets/js/: theme.js (flash prevention + the theme / high-contrast toggles, loaded synchronously so the stored theme lands before first paint), app.js (toast, OTP autosubmit, profile-menu dismissal, confirm-on-submit, copy-to-clipboard, dialogs, the feedback launcher, delayed redirect/reload), avatar-picker.js, sse.js, password.js, and admin-users.js. The markup now opts in through data-* attributes handled by delegated listeners, so server-supplied values (dialog ids, copy commands, confirm text, the SSE origin) reach the browser as passive attributes instead of interpolated JavaScript.

bunyip-web/src/security.rs tightens to script-src 'self' and drops https://ka-f.fontawesome.com from font-src and connect-src now that the icon fonts are same-origin. The Google Fonts stylesheet stays remote: it is style-src, not script-src, and self-hosting those two families is a separate change.

Two tests enforce the removal mechanically. policy_script_src_is_self_only pins the directive to exactly 'self' and asserts no CDN host survives anywhere in the policy. no_inline_script_or_event_handlers_in_views scans every non-test line of bunyip-web/src for an inline <script> body, an on*= attribute, or an off-origin <script src> and fails with file:line if one reappears. document_head_loads_only_first_party_scripts asserts the same at render time.

#BUNYIP-424

bunyip-web loaded htmx from unpkg.com and Font Awesome from its kit loader at kit.fontawesome.com, neither with an `integrity` attribute, and its CSP carried `script-src 'self' 'unsafe-inline' https://unpkg.com https://kit.fontawesome.com`. That was a standing remote-code-execution grant on the origin that holds the session: the kit rotates its own contents by design and cannot carry SRI, unpkg resolves htmx from npm at request time, and `'unsafe-inline'` meant CSP was no barrier to any future reflected-XSS bug in the SSR pages either. The cookies are httpOnly, but injected JS does not need to read them - it issues same-origin fetches the browser attaches them to. Both libraries are now vendored under `bunyip-web/assets/vendor/` with the version in the path so an upgrade is a visible diff: htmx 2.0.3 (byte-identical to the published dist, sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq) and the Font Awesome 6.7.2 Free webfont build (core + solid + regular CSS and their woff2/ttf faces) replacing the kit loader. Every inline `<script>` body and every `on*=` handler moved into `bunyip-web/assets/js/`: theme.js (flash prevention + the theme / high-contrast toggles, loaded synchronously so the stored theme lands before first paint), app.js (toast, OTP autosubmit, profile-menu dismissal, confirm-on-submit, copy-to-clipboard, dialogs, the feedback launcher, delayed redirect/reload), avatar-picker.js, sse.js, password.js, and admin-users.js. The markup now opts in through data-* attributes handled by delegated listeners, so server-supplied values (dialog ids, copy commands, confirm text, the SSE origin) reach the browser as passive attributes instead of interpolated JavaScript. `bunyip-web/src/security.rs` tightens to `script-src 'self'` and drops `https://ka-f.fontawesome.com` from font-src and connect-src now that the icon fonts are same-origin. The Google Fonts stylesheet stays remote: it is style-src, not script-src, and self-hosting those two families is a separate change. Two tests enforce the removal mechanically. `policy_script_src_is_self_only` pins the directive to exactly `'self'` and asserts no CDN host survives anywhere in the policy. `no_inline_script_or_event_handlers_in_views` scans every non-test line of bunyip-web/src for an inline `<script>` body, an `on*=` attribute, or an off-origin `<script src>` and fails with file:line if one reappears. `document_head_loads_only_first_party_scripts` asserts the same at render time. #BUNYIP-424
fix(web): self-host CDN scripts and drop script-src 'unsafe-inline'
All checks were successful
E2E PR gate / Install + reachability (no deployment secrets) (pull_request) Successful in 13s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m19s
Create release / Create release from merged PR (pull_request) Has been skipped
6357b8d346
bunyip-web loaded htmx from unpkg.com and Font Awesome from its kit loader at kit.fontawesome.com, neither with an `integrity` attribute, and its CSP carried `script-src 'self' 'unsafe-inline' https://unpkg.com https://kit.fontawesome.com`. That was a standing remote-code-execution grant on the origin that holds the session: the kit rotates its own contents by design and cannot carry SRI, unpkg resolves htmx from npm at request time, and `'unsafe-inline'` meant CSP was no barrier to any future reflected-XSS bug in the SSR pages either. The cookies are httpOnly, but injected JS does not need to read them - it issues same-origin fetches the browser attaches them to.

Both libraries are now vendored under `bunyip-web/assets/vendor/` with the version in the path so an upgrade is a visible diff: htmx 2.0.3 (byte-identical to the published dist, sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq) and the Font Awesome 6.7.2 Free webfont build (core + solid + regular CSS and their woff2/ttf faces) replacing the kit loader.

Every inline `<script>` body and every `on*=` handler moved into `bunyip-web/assets/js/`: theme.js (flash prevention + the theme / high-contrast toggles, loaded synchronously so the stored theme lands before first paint), app.js (toast, OTP autosubmit, profile-menu dismissal, confirm-on-submit, copy-to-clipboard, dialogs, the feedback launcher, delayed redirect/reload), avatar-picker.js, sse.js, password.js, and admin-users.js. The markup now opts in through data-* attributes handled by delegated listeners, so server-supplied values (dialog ids, copy commands, confirm text, the SSE origin) reach the browser as passive attributes instead of interpolated JavaScript.

`bunyip-web/src/security.rs` tightens to `script-src 'self'` and drops `https://ka-f.fontawesome.com` from font-src and connect-src now that the icon fonts are same-origin. The Google Fonts stylesheet stays remote: it is style-src, not script-src, and self-hosting those two families is a separate change.

Two tests enforce the removal mechanically. `policy_script_src_is_self_only` pins the directive to exactly `'self'` and asserts no CDN host survives anywhere in the policy. `no_inline_script_or_event_handlers_in_views` scans every non-test line of bunyip-web/src for an inline `<script>` body, an `on*=` attribute, or an off-origin `<script src>` and fails with file:line if one reappears. `document_head_loads_only_first_party_scripts` asserts the same at render time.

#BUNYIP-424
Claude-Run deleted branch fix/BUNYIP-424-self-host-scripts-drop-unsafe-inline 2026-07-31 18:40:32 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!422
No description provided.