Fix 7 frontend security vulnerabilities (4 critical, 3 high)

- Escape all innerHTML assignments with user/external data across 12 JS files
- Upgrade credential encryption: per-value IV, key moved to sessionStorage
- Fix open redirect in TOTP auth via proper URL hostname validation
- Remove sensitive DNS topology data from localStorage cache
- Add security regression test suite (51 tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 01:29:04 -08:00
parent 59b6d7d360
commit 52577b11ed
13 changed files with 874 additions and 96 deletions

View File

@@ -284,7 +284,7 @@
const header = document.createElement('div');
header.className = 'app-category-header';
const categoryInfo = apiCategories?.[category] || {};
header.innerHTML = `${categoryInfo.icon || ''} ${category}`;
header.innerHTML = `${escapeHtml(categoryInfo.icon || '')} ${escapeHtml(category)}`;
if (categoryInfo.color) {
header.style.borderBottomColor = categoryInfo.color;
}
@@ -310,7 +310,7 @@
}20; color: ${
app.difficulty === 'Easy' ? '#2ecc71' :
app.difficulty === 'Intermediate' ? '#f39c12' : '#e74c3c'
};">${app.difficulty}</div>` : '';
};">${escapeHtml(app.difficulty)}</div>` : '';
option.innerHTML = `
<div class="app-option-icon">${escapeHtml(app.icon || '📦')}</div>
@@ -488,7 +488,7 @@
btn.type = 'button';
const isSelected = autoPaths.includes(mount.hostPath);
btn.style.cssText = `padding: 8px 14px; font-size: 0.85rem; background: color-mix(in srgb, var(--success) ${isSelected ? '40%' : '15%'}, var(--card-bg)); border: 1px solid var(--success); border-radius: 6px; cursor: pointer; color: var(--fg);`;
btn.innerHTML = `<span style="font-weight: 500;">${mount.folderName}</span><br><span style="font-size: 0.7rem; color: var(--muted);">from ${mount.sourceImage}</span>`;
btn.innerHTML = `<span style="font-weight: 500;">${escapeHtml(mount.folderName)}</span><br><span style="font-size: 0.7rem; color: var(--muted);">from ${escapeHtml(mount.sourceImage)}</span>`;
btn.title = `${mount.hostPath} (from ${mount.sourceContainer})`;
btn.onclick = () => {
const currentPaths = mediaPathInput.value.split(',').map(p => p.trim()).filter(p => p);