From 6253810a9fcb01bb187af2c26f70fb0dc62c69c7 Mon Sep 17 00:00:00 2001 From: Fabien Penso Date: Wed, 11 Feb 2026 17:01:03 -0800 Subject: [PATCH] fix(csp): eliminate inline scripts to fix Safari CSP false-positives Move executable inline scripts to external files or non-executable types. Only the import map remains inline with a CSP nonce. - Extract theme-init IIFE to external js/theme-init.js - Convert gon data from window.__MOLTIS__ assignment to type="application/json" blob parsed by gon.js - Remove inline favicon and title-update scripts (already handled by app.js applyIdentity / applyIdentityFavicon) - Remove nonce from external module scripts (allowed by 'self') - Add applyIdentityFavicon to login-app.js - Add theme-init.js to service worker cache CSP policy unchanged: script-src 'self' 'nonce-{uuid}' --- CHANGELOG.md | 8 +++ crates/gateway/src/assets/index.html | 8 ++- crates/gateway/src/assets/js/gon.js | 11 ++-- crates/gateway/src/assets/js/login-app.js | 6 ++- crates/gateway/src/assets/js/theme-init.js | 7 +++ crates/gateway/src/assets/login.html | 7 ++- crates/gateway/src/assets/onboarding.html | 4 +- crates/gateway/src/assets/sw.js | 1 + crates/gateway/src/server.rs | 63 ++++++++++++++++------ 9 files changed, 81 insertions(+), 34 deletions(-) create mode 100644 crates/gateway/src/assets/js/theme-init.js diff --git a/CHANGELOG.md b/CHANGELOG.md index cbbe6eb8..30624a78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- **CSP inline script elimination**: Moved inline scripts to external files; + only the import map remains inline with a CSP nonce, eliminating Safari CSP + false-positive violations. + ## [0.8.8] - 2026-02-11 ### Changed diff --git a/crates/gateway/src/assets/index.html b/crates/gateway/src/assets/index.html index 7272e7ce..4cfbcbda 100644 --- a/crates/gateway/src/assets/index.html +++ b/crates/gateway/src/assets/index.html @@ -16,7 +16,7 @@ - + {{ share_site_name }} @@ -37,8 +37,7 @@ - - + disconnected
@@ -538,6 +536,6 @@

No LLMs Connected

- + diff --git a/crates/gateway/src/assets/js/gon.js b/crates/gateway/src/assets/js/gon.js index 114d3c00..573f90fd 100644 --- a/crates/gateway/src/assets/js/gon.js +++ b/crates/gateway/src/assets/js/gon.js @@ -1,14 +1,15 @@ // ── Server-injected data (gon pattern) ──────────────────── // -// The server injects `window.__MOLTIS__ = { ... }` into every -// page before any module script runs. This module -// provides typed access, runtime updates, and a refresh -// mechanism that re-fetches the data from `/api/gon`. +// The server injects a ` + {{ page_title }} - - + + diff --git a/crates/gateway/src/assets/onboarding.html b/crates/gateway/src/assets/onboarding.html index 49a6ac48..f8b89382 100644 --- a/crates/gateway/src/assets/onboarding.html +++ b/crates/gateway/src/assets/onboarding.html @@ -4,7 +4,7 @@ - + {{ page_title }} @@ -25,6 +25,6 @@
- + diff --git a/crates/gateway/src/assets/sw.js b/crates/gateway/src/assets/sw.js index 52d81707..ed6a70ac 100644 --- a/crates/gateway/src/assets/sw.js +++ b/crates/gateway/src/assets/sw.js @@ -4,6 +4,7 @@ var CACHE_NAME = "moltis-v2"; var STATIC_ASSETS = [ "/manifest.json", + "/assets/js/theme-init.js", "/assets/css/base.css", "/assets/css/layout.css", "/assets/css/chat.css", diff --git a/crates/gateway/src/server.rs b/crates/gateway/src/server.rs index 003aa919..68f8122d 100644 --- a/crates/gateway/src/server.rs +++ b/crates/gateway/src/server.rs @@ -5711,11 +5711,14 @@ mod tests { assert!(html.contains("/assets/v/test/js/onboarding-app.js")); assert!(!html.contains("/assets/v/test/js/app.js")); assert!(!html.contains("/manifest.json")); - assert!(html.contains("")); + // Import map still requires a nonce. assert!(html.contains("")); + // Gon data is now a non-executable JSON blob. assert!(html.contains( - "" + "" )); + // External module script does NOT need a nonce. + assert!( + html.contains( + "" + ) + ); + // Theme init is external. + assert!(html.contains("")); + // Inline favicon script was removed. + assert!(!html.contains("var svg=")); + // Import map still has nonce. + assert!(html.contains("" - ))); + // Theme init is now an external script (no nonce needed, allowed by 'self'). + assert!(index_html.contains("")); + // Gon data is a non-executable JSON blob (no nonce needed). + assert!(index_html.contains("" - ))); + // External module script does NOT need a nonce (allowed by 'self'). + assert!( + index_html + .contains("") + ); + // Inline title-update script was removed (handled by app.js). + assert!(!index_html.contains("document.title=a}()")); let onboarding_template = OnboardingHtmlTemplate { build_ts: "dev", @@ -6155,9 +6177,18 @@ mod tests { Ok(html) => html, Err(e) => panic!("failed to render onboarding template: {e}"), }; - assert!(onboarding_html.contains(&format!( - "" - ))); + // Onboarding: external module script does NOT need a nonce. + assert!(onboarding_html.contains( + "" + )); + // Onboarding: theme init is external. + assert!( + onboarding_html.contains("") + ); + // Onboarding: import map still has nonce. + assert!( + onboarding_html.contains(&format!("