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:
@@ -135,7 +135,7 @@
|
||||
resultDiv.style.background = 'color-mix(in srgb, var(--ok-fg) 15%, transparent)';
|
||||
resultDiv.style.border = '1px solid var(--ok-fg)';
|
||||
} catch (e) {
|
||||
resultDiv.innerHTML = `❌ Export failed: ${e.message}`;
|
||||
resultDiv.innerHTML = `❌ Export failed: ${escapeHtml(e.message)}`;
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.style.background = 'color-mix(in srgb, var(--bad-fg) 15%, transparent)';
|
||||
resultDiv.style.border = '1px solid var(--bad-fg)';
|
||||
@@ -179,14 +179,14 @@
|
||||
previewContent.innerHTML = html;
|
||||
previewDiv.style.display = 'block';
|
||||
} else {
|
||||
resultDiv.innerHTML = `⚠️ Invalid backup file: ${preview.error}`;
|
||||
resultDiv.innerHTML = `⚠️ Invalid backup file: ${escapeHtml(preview.error)}`;
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.style.background = 'color-mix(in srgb, #f39c12 15%, transparent)';
|
||||
resultDiv.style.border = '1px solid #f39c12';
|
||||
previewDiv.style.display = 'none';
|
||||
}
|
||||
} catch (e) {
|
||||
resultDiv.innerHTML = `❌ Could not read file: ${e.message}`;
|
||||
resultDiv.innerHTML = `❌ Could not read file: ${escapeHtml(e.message)}`;
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.style.background = 'color-mix(in srgb, var(--bad-fg) 15%, transparent)';
|
||||
resultDiv.style.border = '1px solid var(--bad-fg)';
|
||||
@@ -216,16 +216,16 @@
|
||||
resultDiv.style.border = '1px solid var(--ok-fg)';
|
||||
setTimeout(() => location.reload(), 2000);
|
||||
} else {
|
||||
resultDiv.innerHTML = `⚠️ ${data.message}`;
|
||||
resultDiv.innerHTML = `⚠️ ${escapeHtml(data.message)}`;
|
||||
if (data.results?.errors?.length > 0) {
|
||||
resultDiv.innerHTML += '<br><small>' + data.results.errors.map(e => `${e.file}: ${e.error}`).join(', ') + '</small>';
|
||||
resultDiv.innerHTML += '<br><small>' + data.results.errors.map(e => `${escapeHtml(e.file)}: ${escapeHtml(e.error)}`).join(', ') + '</small>';
|
||||
}
|
||||
resultDiv.style.background = 'color-mix(in srgb, #f39c12 15%, transparent)';
|
||||
resultDiv.style.border = '1px solid #f39c12';
|
||||
}
|
||||
resultDiv.style.display = 'block';
|
||||
} catch (e) {
|
||||
resultDiv.innerHTML = `❌ Restore failed: ${e.message}`;
|
||||
resultDiv.innerHTML = `❌ Restore failed: ${escapeHtml(e.message)}`;
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.style.background = 'color-mix(in srgb, var(--bad-fg) 15%, transparent)';
|
||||
resultDiv.style.border = '1px solid var(--bad-fg)';
|
||||
@@ -280,7 +280,7 @@
|
||||
document.getElementById('backup-save-schedule')?.addEventListener('click', saveSchedule);
|
||||
document.getElementById('backup-run-now')?.addEventListener('click', runBackupNow);
|
||||
} catch (e) {
|
||||
scheduleContainer.innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed to load schedule: ${e.message}</div>`;
|
||||
scheduleContainer.innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed to load schedule: ${escapeHtml(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,7 +309,7 @@
|
||||
});
|
||||
const data = await res.json();
|
||||
if (resultEl) {
|
||||
resultEl.innerHTML = data.success ? '✅ Schedule saved' : `⚠️ ${data.error}`;
|
||||
resultEl.innerHTML = data.success ? '✅ Schedule saved' : `⚠️ ${escapeHtml(data.error)}`;
|
||||
resultEl.style.display = 'block';
|
||||
resultEl.style.background = data.success ? 'color-mix(in srgb, var(--ok-fg) 15%, transparent)' : 'color-mix(in srgb, var(--bad-fg) 15%, transparent)';
|
||||
resultEl.style.border = data.success ? '1px solid var(--ok-fg)' : '1px solid var(--bad-fg)';
|
||||
@@ -317,7 +317,7 @@
|
||||
}
|
||||
} catch (e) {
|
||||
if (resultEl) {
|
||||
resultEl.innerHTML = `❌ ${e.message}`;
|
||||
resultEl.innerHTML = `❌ ${escapeHtml(e.message)}`;
|
||||
resultEl.style.display = 'block';
|
||||
resultEl.style.background = 'color-mix(in srgb, var(--bad-fg) 15%, transparent)';
|
||||
resultEl.style.border = '1px solid var(--bad-fg)';
|
||||
@@ -343,7 +343,7 @@
|
||||
resultEl.style.background = 'color-mix(in srgb, var(--ok-fg) 15%, transparent)';
|
||||
resultEl.style.border = '1px solid var(--ok-fg)';
|
||||
} else {
|
||||
resultEl.innerHTML = `⚠️ ${data.error}`;
|
||||
resultEl.innerHTML = `⚠️ ${escapeHtml(data.error)}`;
|
||||
resultEl.style.background = 'color-mix(in srgb, var(--bad-fg) 15%, transparent)';
|
||||
resultEl.style.border = '1px solid var(--bad-fg)';
|
||||
}
|
||||
@@ -352,7 +352,7 @@
|
||||
loadBackupHistory();
|
||||
} catch (e) {
|
||||
if (resultEl) {
|
||||
resultEl.innerHTML = `❌ ${e.message}`;
|
||||
resultEl.innerHTML = `❌ ${escapeHtml(e.message)}`;
|
||||
resultEl.style.display = 'block';
|
||||
resultEl.style.background = 'color-mix(in srgb, var(--bad-fg) 15%, transparent)';
|
||||
resultEl.style.border = '1px solid var(--bad-fg)';
|
||||
@@ -378,10 +378,10 @@
|
||||
const sizeMB = bk.size ? (bk.size / 1024 / 1024).toFixed(2) : '?';
|
||||
html += `<div style="padding: 10px 12px; background: var(--card-base); border-radius: 6px; border: 1px solid var(--border); font-size: 0.85rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
|
||||
<span style="font-weight: 500;">${bk.name || 'backup'}</span>
|
||||
<span style="font-weight: 500;">${escapeHtml(bk.name || 'backup')}</span>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span class="status-badge ${bk.status === 'success' ? 'success' : 'down'}">${bk.status}</span>
|
||||
${bk.status === 'success' ? `<button onclick="window.__restoreServerBackup('${bk.id}')" style="padding: 3px 8px; font-size: 0.75rem;">Restore</button>` : ''}
|
||||
<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>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size: 0.75rem; color: var(--muted);">
|
||||
@@ -393,7 +393,7 @@
|
||||
html += '</div>';
|
||||
historyContainer.innerHTML = html;
|
||||
} catch (e) {
|
||||
historyContainer.innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed: ${e.message}</div>`;
|
||||
historyContainer.innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed: ${escapeHtml(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user