Skip to content

Commit 4a33817

Browse files
committed
fix: pin CDN scripts + theme CSS with SRI integrity hashes (closes #19)
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 <link> 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/<name>/<version>?fields=sri' 2. Re-derived from actual CDN content curl -sL <url> | 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.
1 parent ceefe1c commit 4a33817

2 files changed

Lines changed: 49 additions & 17 deletions

File tree

static/index.html

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,24 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>Claude Code Chat Browser</title>
77
<link rel="stylesheet" href="/static/css/style.css">
8-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.min.css" id="hljs-theme">
9-
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
10-
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
8+
<!-- SRI hashes pin each CDN asset to a specific known-good payload; the
9+
browser refuses anything whose hash does not match (issue #19).
10+
crossorigin="anonymous" is required for the browser to enforce SRI
11+
on cross-origin requests. Hashes verified against cdnjs's SRI API:
12+
curl https://api.cdnjs.com/libraries/<name>/<version>?fields=sri
13+
NOTE: the runtime theme swap in static/js/app.js MUST also swap the
14+
integrity attribute when it changes href — see HLJS_THEME_SHEETS. -->
15+
<link rel="stylesheet"
16+
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.min.css"
17+
integrity="sha512-mtXspRdOWHCYp+f4c7CkWGYPPRAhq9X+xCvJMUBVAb6pqA4U8pxhT3RWT3LP3bKbiolYL2CkL1bSKZZO4eeTew=="
18+
crossorigin="anonymous"
19+
id="hljs-theme">
20+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"
21+
integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ=="
22+
crossorigin="anonymous"></script>
23+
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"
24+
integrity="sha512-pSeTnZAQF/RHxb0ysMoYQI/BRZsa5XuklcrgFfU3YZIdnD3LvkkqzrIeHxzFi6gKtI8Cpq2DEWdZjMTcNVhUYA=="
25+
crossorigin="anonymous"></script>
1126
</head>
1227
<body>
1328
<!-- Navbar -->

static/js/app.js

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
// Claude Code Chat Browser — Main JS
22

3+
// Highlight.js theme stylesheets, keyed by theme name. Both `href` and
4+
// `integrity` MUST be assigned together when swapping at runtime —
5+
// changing `href` while leaving a stale `integrity` would make the
6+
// browser refuse the new stylesheet and break the UI (issue #19).
7+
// Hashes verified against cdnjs's SRI API. The corresponding static
8+
// tag in static/index.html carries crossorigin="anonymous" which
9+
// persists across runtime href swaps.
10+
const HLJS_THEME_SHEETS = {
11+
dark: {
12+
href: 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.min.css',
13+
integrity: 'sha512-mtXspRdOWHCYp+f4c7CkWGYPPRAhq9X+xCvJMUBVAb6pqA4U8pxhT3RWT3LP3bKbiolYL2CkL1bSKZZO4eeTew==',
14+
},
15+
light: {
16+
href: 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css',
17+
integrity: 'sha512-0aPQyyeZrWj9sCA46UlmWgKOP0mUipLQ6OZXu8l4IcAmD2u31EPEy9VcIMvl7SoAaKe8bLXZhYoMaE/in+gcgA==',
18+
},
19+
};
20+
21+
function applyHljsTheme(themeName) {
22+
const link = document.getElementById('hljs-theme');
23+
if (!link) return;
24+
const sheet = HLJS_THEME_SHEETS[themeName] || HLJS_THEME_SHEETS.dark;
25+
// Set integrity FIRST, then href — the browser reads the current
26+
// integrity at fetch time, and href change is what triggers the fetch.
27+
link.integrity = sheet.integrity;
28+
link.href = sheet.href;
29+
}
30+
331
function showToast(message, type = 'info') {
432
const icons = { success: '\u2713', error: '\u2717', info: '\u2139' };
533
const toast = document.createElement('div');
@@ -122,14 +150,8 @@ function setHamburgerVisible(visible) {
122150
function setWorkspaceMode(active) {
123151
// No container class change needed — workspace lives inside the standard container
124152
document.body.classList.toggle('workspace-mode', active);
125-
// Switch highlight.js theme
126-
const hljsLink = document.getElementById('hljs-theme');
127-
if (hljsLink) {
128-
const theme = localStorage.getItem('theme') || 'dark';
129-
hljsLink.href = theme === 'dark'
130-
? 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.min.css'
131-
: 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css';
132-
}
153+
// Switch highlight.js theme — helper updates href + integrity together (issue #19).
154+
applyHljsTheme(localStorage.getItem('theme') || 'dark');
133155
}
134156

135157
let _navInProgress = false;
@@ -836,12 +858,7 @@ function applyTheme(theme) {
836858
moon.style.display = theme === 'dark' ? 'block' : 'none';
837859
sun.style.display = theme === 'light' ? 'block' : 'none';
838860
}
839-
const hljsLink = document.getElementById('hljs-theme');
840-
if (hljsLink) {
841-
hljsLink.href = theme === 'dark'
842-
? 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.min.css'
843-
: 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css';
844-
}
861+
applyHljsTheme(theme); // href + integrity swapped together (issue #19)
845862
}
846863

847864
function toggleTheme() {

0 commit comments

Comments
 (0)