From 4a3381711bebf6e7c386b5719a81845ea1a98a80 Mon Sep 17 00:00:00 2001 From: timon0305 Date: Tue, 5 May 2026 18:07:04 +0200 Subject: [PATCH 1/2] fix: pin CDN scripts + theme CSS with SRI integrity hashes (closes #19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard loads three cross-origin assets without integrity attributes: highlight.js/11.9.0/styles/vs2015.min.css highlight.js/11.9.0/highlight.min.js marked/12.0.1/marked.min.js Marked.js parses raw markdown and highlight.js runs on every code block — a swapped CDN payload would execute in our origin with full DOM access. Subresource Integrity (sha512 hash + crossorigin="anonymous") makes the browser refuse anything whose hash does not match a known-good value. Wrinkle: static/js/app.js used to swap `hljsLink.href` between the dark (vs2015) and light (github) stylesheets at runtime via a plain ternary. Adding integrity to the static tag would have made the runtime swap break — the browser reads the (now-stale) integrity at fetch time and refuses the new sheet. Fix: introduced HLJS_THEME_SHEETS const map keyed by theme name, each entry carrying { href, integrity }. New applyHljsTheme(theme) helper sets integrity FIRST then href so the browser sees the right hash when it triggers the new fetch. Both inline ternary call sites (applyTheme and setWorkspaceMode) now call the helper instead of duplicating the logic. All four SHA-512 hashes verified two ways: 1. cdnjs SRI API gh api 'https://api.cdnjs.com/libraries//?fields=sri' 2. Re-derived from actual CDN content curl -sL | openssl dgst -sha512 -binary | base64 → all four match. pytest 75/75 OK (no behaviour change in Python code). Live HTTP smoke: served HTML carries integrity + crossorigin on all three tags; manual browser verification path documented in PR body. --- static/index.html | 21 ++++++++++++++++++--- static/js/app.js | 45 +++++++++++++++++++++++++++++++-------------- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/static/index.html b/static/index.html index 29c39fe..9f02734 100644 --- a/static/index.html +++ b/static/index.html @@ -5,9 +5,24 @@ Claude Code Chat Browser - - - + + + + diff --git a/static/js/app.js b/static/js/app.js index 262e80e..c3edcd8 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1,5 +1,33 @@ // Claude Code Chat Browser — Main JS +// Highlight.js theme stylesheets, keyed by theme name. Both `href` and +// `integrity` MUST be assigned together when swapping at runtime — +// changing `href` while leaving a stale `integrity` would make the +// browser refuse the new stylesheet and break the UI (issue #19). +// Hashes verified against cdnjs's SRI API. The corresponding static +// tag in static/index.html carries crossorigin="anonymous" which +// persists across runtime href swaps. +const HLJS_THEME_SHEETS = { + dark: { + href: 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.min.css', + integrity: 'sha512-mtXspRdOWHCYp+f4c7CkWGYPPRAhq9X+xCvJMUBVAb6pqA4U8pxhT3RWT3LP3bKbiolYL2CkL1bSKZZO4eeTew==', + }, + light: { + href: 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css', + integrity: 'sha512-0aPQyyeZrWj9sCA46UlmWgKOP0mUipLQ6OZXu8l4IcAmD2u31EPEy9VcIMvl7SoAaKe8bLXZhYoMaE/in+gcgA==', + }, +}; + +function applyHljsTheme(themeName) { + const link = document.getElementById('hljs-theme'); + if (!link) return; + const sheet = HLJS_THEME_SHEETS[themeName] || HLJS_THEME_SHEETS.dark; + // Set integrity FIRST, then href — the browser reads the current + // integrity at fetch time, and href change is what triggers the fetch. + link.integrity = sheet.integrity; + link.href = sheet.href; +} + function showToast(message, type = 'info') { const icons = { success: '\u2713', error: '\u2717', info: '\u2139' }; const toast = document.createElement('div'); @@ -122,14 +150,8 @@ function setHamburgerVisible(visible) { function setWorkspaceMode(active) { // No container class change needed — workspace lives inside the standard container document.body.classList.toggle('workspace-mode', active); - // Switch highlight.js theme - const hljsLink = document.getElementById('hljs-theme'); - if (hljsLink) { - const theme = localStorage.getItem('theme') || 'dark'; - hljsLink.href = theme === 'dark' - ? 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.min.css' - : 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css'; - } + // Switch highlight.js theme — helper updates href + integrity together (issue #19). + applyHljsTheme(localStorage.getItem('theme') || 'dark'); } let _navInProgress = false; @@ -836,12 +858,7 @@ function applyTheme(theme) { moon.style.display = theme === 'dark' ? 'block' : 'none'; sun.style.display = theme === 'light' ? 'block' : 'none'; } - const hljsLink = document.getElementById('hljs-theme'); - if (hljsLink) { - hljsLink.href = theme === 'dark' - ? 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.min.css' - : 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css'; - } + applyHljsTheme(theme); // href + integrity swapped together (issue #19) } function toggleTheme() { From 1f3cc09121b571640365f688d206442da90660e1 Mon Sep 17 00:00:00 2001 From: timon0305 Date: Tue, 5 May 2026 20:24:27 +0200 Subject: [PATCH 2/2] ci: empty retrigger to see if any workflow runs on PR #20