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

@@ -25,12 +25,13 @@ const DC = {
};
// ===== GLOBAL SITE CONFIG (loaded from server, cached in localStorage) =====
// Only non-sensitive display preferences are cached; DNS IPs/topology are fetched from API
const _cachedCfg = JSON.parse(localStorage.getItem('dashcaddy_site_config') || 'null');
const SITE = {
tld: (_cachedCfg && _cachedCfg.tld) || '.home',
dnsIp: (_cachedCfg && _cachedCfg.dnsIp) || '',
dnsPort: (_cachedCfg && _cachedCfg.dnsPort) || DC.DEFAULTS.DNS_PORT,
dnsServers: (_cachedCfg && _cachedCfg.dnsServers) || {},
dnsIp: '',
dnsPort: DC.DEFAULTS.DNS_PORT,
dnsServers: {},
configurationType: (_cachedCfg && _cachedCfg.configurationType) || 'homelab',
domain: (_cachedCfg && _cachedCfg.domain) || '',
defaults: (_cachedCfg && _cachedCfg.defaults) || {},
@@ -53,11 +54,11 @@ const SITE = {
if (c.domain) SITE.domain = c.domain;
if (c.defaults) SITE.defaults = c.defaults;
if (c.routingMode) SITE.routingMode = c.routingMode;
// Cache config so next page load uses correct TLD even if API is slow
// Cache only non-sensitive display config (TLD, domain, routing mode)
// DNS IPs and server topology are NOT cached — fetched from API each load
localStorage.setItem('dashcaddy_site_config', JSON.stringify({
tld: SITE.tld, dnsIp: SITE.dnsIp, dnsPort: SITE.dnsPort, dnsServers: SITE.dnsServers,
configurationType: SITE.configurationType, domain: SITE.domain, defaults: SITE.defaults,
routingMode: SITE.routingMode
tld: SITE.tld, configurationType: SITE.configurationType,
domain: SITE.domain, routingMode: SITE.routingMode
}));
// Render DNS cards dynamically based on configured servers
renderDnsCards();
@@ -100,23 +101,24 @@ function renderDnsCards() {
const firstChild = topRow.firstElementChild;
dnsIds.forEach(id => {
const label = (SITE.dnsServers[id].name || id).toUpperCase();
const safeId = escapeHtml(id);
const label = escapeHtml((SITE.dnsServers[id].name || id).toUpperCase());
const card = document.createElement('div');
card.className = 'card';
card.setAttribute('data-app', id);
card.setAttribute('data-status', 'off');
card.innerHTML =
`<span id="${id}-dot" class="dot bad at-bl"></span>`
`<span id="${safeId}-dot" class="dot bad at-bl"></span>`
+ `<div class="row"><div class="logo-wrap">${svgIcon}</div>`
+ `<span class="name">${label}</span><span class="spacer"></span>`
+ `<span id="${id}-pill" class="badge off">OFF</span></div>`
+ `<div class="response-row"><span id="${id}-time" class="response-time">--</span></div>`
+ `<span id="${safeId}-pill" class="badge off">OFF</span></div>`
+ `<div class="response-row"><span id="${safeId}-time" class="response-time">--</span></div>`
+ `<div class="btn-row">`
+ `<button id="${id}-restart" class="restart-btn">Restart</button>`
+ `<button id="${id}-update" class="update-btn" title="Update DNS server">⬆️</button>`
+ `<button id="${id}-open">Open</button>`
+ `<button id="${id}-logs" class="logs-btn">Logs</button>`
+ `<button id="${id}-settings" class="settings-btn">⚙️</button>`
+ `<button id="${safeId}-restart" class="restart-btn">Restart</button>`
+ `<button id="${safeId}-update" class="update-btn" title="Update DNS server">⬆️</button>`
+ `<button id="${safeId}-open">Open</button>`
+ `<button id="${safeId}-logs" class="logs-btn">Logs</button>`
+ `<button id="${safeId}-settings" class="settings-btn">⚙️</button>`
+ `</div>`;
topRow.insertBefore(card, firstChild);
});