Fix remaining frontend security issues (3 medium, 2 low)

- Escape user-input port number in app-selector innerHTML
- Replace inline onclick with addEventListener in backup history (HTML entity decode bypass)
- Add Content-Security-Policy meta tag with script hash
- Replace document.write with textContent for footer year
- Filter __proto__/constructor/prototype in Object.assign calls

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 02:06:55 -08:00
parent 52577b11ed
commit 9a0abc02d1
4 changed files with 20 additions and 6 deletions

View File

@@ -8,6 +8,7 @@
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" /> <meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" /> <meta http-equiv="Expires" content="0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'sha256-6JZtsKK/PZthh+stCmmCvC2QxCiyk6SwZCBjXE+kYr0='; style-src 'self' 'unsafe-inline'; img-src 'self' https://cdn.jsdelivr.net data:; connect-src 'self' https://api.open-meteo.com https://geocoding-api.open-meteo.com; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'">
<link rel="icon" href="/assets/dashcaddy-favicon.ico" sizes="any"> <link rel="icon" href="/assets/dashcaddy-favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/icon-192.png"> <link rel="icon" type="image/png" sizes="192x192" href="/assets/icon-192.png">
@@ -534,13 +535,15 @@
if (el) el.style.display = 'none'; if (el) el.style.display = 'none';
} }
} }
var yr = document.getElementById('footer-year');
if (yr) yr.textContent = new Date().getFullYear();
})(); })();
</script> </script>
<!-- Clock rendering is handled by the bundled clock.js module --> <!-- Clock rendering is handled by the bundled clock.js module -->
<footer class="dashcaddy-footer"> <footer class="dashcaddy-footer">
<span class="footer-copy">&copy; <script>document.write(new Date().getFullYear())</script></span> <span class="footer-copy">&copy; <span id="footer-year"></span></span>
<img src="/assets/sami7777-logo.png" alt="samiahmed7777" class="footer-logo"> <img src="/assets/sami7777-logo.png" alt="samiahmed7777" class="footer-logo">
</footer> </footer>

View File

@@ -587,7 +587,7 @@
const result = await checkPortAvailability(portToCheck); const result = await checkPortAvailability(portToCheck);
if (result.available) { if (result.available) {
portStatus.innerHTML = `<span style="color: #4caf50;">Port ${portToCheck} is available</span>`; portStatus.innerHTML = `<span style="color: #4caf50;">Port ${escapeHtml(String(portToCheck))} is available</span>`;
} else { } else {
const suggestedPort = await getSuggestedPort(defaultPort); const suggestedPort = await getSuggestedPort(defaultPort);
portStatus.innerHTML = ` portStatus.innerHTML = `

View File

@@ -381,7 +381,7 @@
<span style="font-weight: 500;">${escapeHtml(bk.name || 'backup')}</span> <span style="font-weight: 500;">${escapeHtml(bk.name || 'backup')}</span>
<div style="display: flex; align-items: center; gap: 8px;"> <div style="display: flex; align-items: center; gap: 8px;">
<span class="status-badge ${bk.status === 'success' ? 'success' : 'down'}">${escapeHtml(bk.status)}</span> <span class="status-badge ${bk.status === 'success' ? 'success' : 'down'}">${escapeHtml(bk.status)}</span>
${bk.status === 'success' ? `<button onclick="window.__restoreServerBackup('${escapeHtml(bk.id)}')" style="padding: 3px 8px; font-size: 0.75rem;">Restore</button>` : ''} ${bk.status === 'success' ? `<button class="backup-restore-btn" data-backup-id="${escapeHtml(bk.id)}" style="padding: 3px 8px; font-size: 0.75rem;">Restore</button>` : ''}
</div> </div>
</div> </div>
<div style="font-size: 0.75rem; color: var(--muted);"> <div style="font-size: 0.75rem; color: var(--muted);">
@@ -392,6 +392,10 @@
} }
html += '</div>'; html += '</div>';
historyContainer.innerHTML = html; historyContainer.innerHTML = html;
// Wire restore buttons with addEventListener (not inline onclick — HTML entity decode bypass)
historyContainer.querySelectorAll('.backup-restore-btn').forEach(btn => {
btn.addEventListener('click', () => window.__restoreServerBackup(btn.dataset.backupId));
});
} catch (e) { } catch (e) {
historyContainer.innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed: ${escapeHtml(e.message)}</div>`; historyContainer.innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed: ${escapeHtml(e.message)}</div>`;
} }

View File

@@ -47,8 +47,10 @@ const SITE = {
SITE.dnsIp = c.dns.ip || ''; SITE.dnsIp = c.dns.ip || '';
SITE.dnsPort = c.dns.port || DC.DEFAULTS.DNS_PORT; SITE.dnsPort = c.dns.port || DC.DEFAULTS.DNS_PORT;
} }
if (c.dnsServers) { if (c.dnsServers && typeof c.dnsServers === 'object') {
Object.assign(SITE.dnsServers, c.dnsServers); for (const [k, v] of Object.entries(c.dnsServers)) {
if (k !== '__proto__' && k !== 'constructor' && k !== 'prototype') SITE.dnsServers[k] = v;
}
} }
if (c.configurationType) SITE.configurationType = c.configurationType; if (c.configurationType) SITE.configurationType = c.configurationType;
if (c.domain) SITE.domain = c.domain; if (c.domain) SITE.domain = c.domain;
@@ -352,7 +354,12 @@ const AppState = {
}, },
updateApp(id, changes) { updateApp(id, changes) {
const app = this._apps.find(a => a.id === id); const app = this._apps.find(a => a.id === id);
if (app) { Object.assign(app, changes); DC_BUS.emit('apps:changed', this._apps); } if (app) {
for (const [k, v] of Object.entries(changes)) {
if (k !== '__proto__' && k !== 'constructor' && k !== 'prototype') app[k] = v;
}
DC_BUS.emit('apps:changed', this._apps);
}
return app; return app;
} }
}; };