fix: security hardening and cleanup from community PR cherry-picks

- Add HTML sanitizer for markdown rendering (XSS prevention)
- Switch service worker to network-first caching (deploys take effect immediately)
- Sanitize Content-Disposition filenames (header injection prevention)
- Expose session.muxName getter, replace unsafe `as any` cast
- Static import for execFile, update CLAUDE.md keyboard shortcuts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
arkon
2026-04-11 07:20:09 +02:00
co-authored by Claude Opus 4.6
parent f3cbe9bca6
commit b4a808adcf
6 changed files with 49 additions and 14 deletions
+30 -1
View File
@@ -898,11 +898,40 @@ class CodemanApp {
// Response Viewer — native-scroll panel for reading full Claude responses
// ═══════════════════════════════════════════════════════════════
/** Strip dangerous elements and attributes from HTML (XSS prevention) */
_sanitizeHtml(html) {
const tpl = document.createElement('template');
tpl.innerHTML = html;
const frag = tpl.content;
// Remove dangerous elements
for (const el of frag.querySelectorAll('script, iframe, object, embed, form, base, meta, link, style')) {
el.remove();
}
// Strip dangerous attributes from all elements
for (const el of frag.querySelectorAll('*')) {
for (const attr of [...el.attributes]) {
const name = attr.name.toLowerCase();
if (name.startsWith('on')) {
el.removeAttribute(attr.name);
} else if (['href', 'src', 'action', 'xlink:href', 'formaction'].includes(name)) {
const val = attr.value.replace(/\s/g, '').toLowerCase();
if (val.startsWith('javascript:') || val.startsWith('vbscript:') || val.startsWith('data:text/html')) {
el.removeAttribute(attr.name);
}
}
}
}
// Serialize back via a container
const div = document.createElement('div');
div.appendChild(frag);
return div.innerHTML;
}
/** Render markdown to sanitized HTML, falling back to plain text if marked.js unavailable */
_renderMarkdown(text) {
if (typeof marked !== 'undefined' && marked.parse) {
try {
return marked.parse(text, { breaks: true, gfm: true });
return this._sanitizeHtml(marked.parse(text, { breaks: true, gfm: true }));
} catch { /* fall through */ }
}
// Fallback: escape HTML and preserve whitespace
+7 -8
View File
@@ -72,7 +72,9 @@ self.addEventListener('activate', (event) => {
);
});
// --- Fetch: network-first for API/navigation, cache-first for static ---
// --- Fetch: network-first with cache fallback ---
// Network-first ensures deploys take effect immediately when online.
// Cache is only used when the network is unavailable (offline/flaky).
self.addEventListener('fetch', (event) => {
const { request } = event;
@@ -84,18 +86,15 @@ self.addEventListener('fetch', (event) => {
if (request.url.includes('/api/')) return;
event.respondWith(
caches.match(request).then((cached) => {
// Return cache immediately, refresh in background (stale-while-revalidate)
const fetchPromise = fetch(request).then((response) => {
fetch(request)
.then((response) => {
if (response && response.ok) {
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
}
return response;
}).catch(() => cached);
return cached || fetchPromise;
})
})
.catch(() => caches.match(request))
);
});