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:
@@ -137,15 +137,15 @@
|
||||
const u7d = s.uptime?.['7d'] ?? '-';
|
||||
const avgRt = s.avgResponseTime != null ? Math.round(s.avgResponseTime) + 'ms' : '-';
|
||||
const lastCheck = s.timestamp ? timeAgo(s.timestamp) : '-';
|
||||
html += `<tr style="border-bottom: 1px solid var(--border); cursor: pointer;" data-health-id="${s.serviceId}">`;
|
||||
html += `<td style="padding: 8px; font-weight: 500;">${s.name || s.serviceId}</td>`;
|
||||
html += `<tr style="border-bottom: 1px solid var(--border); cursor: pointer;" data-health-id="${escapeHtml(s.serviceId)}">`;
|
||||
html += `<td style="padding: 8px; font-weight: 500;">${escapeHtml(s.name || s.serviceId)}</td>`;
|
||||
html += `<td style="padding: 8px;"><span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${dotColor}; margin-right: 6px;"></span>${isUp ? 'Up' : 'Down'}</td>`;
|
||||
html += `<td style="padding: 8px; color: ${typeof u24 === 'number' ? uptimeColor(u24) : 'var(--muted)'};">${typeof u24 === 'number' ? u24.toFixed(1) + '%' : u24}</td>`;
|
||||
html += `<td style="padding: 8px; color: ${typeof u7d === 'number' ? uptimeColor(u7d) : 'var(--muted)'};">${typeof u7d === 'number' ? u7d.toFixed(1) + '%' : u7d}</td>`;
|
||||
html += `<td style="padding: 8px;">${avgRt}</td>`;
|
||||
html += `<td style="padding: 8px; color: var(--muted);">${lastCheck}</td>`;
|
||||
html += '</tr>';
|
||||
html += `<tr id="health-detail-${s.serviceId}" style="display: none;"><td colspan="6" style="padding: 12px; background: var(--bg); border-bottom: 1px solid var(--border);"><div class="panel-empty"><span class="brand-spinner"></span> Loading details...</div></td></tr>`;
|
||||
html += `<tr id="health-detail-${escapeHtml(s.serviceId)}" style="display: none;"><td colspan="6" style="padding: 12px; background: var(--bg); border-bottom: 1px solid var(--border);"><div class="panel-empty"><span class="brand-spinner"></span> Loading details...</div></td></tr>`;
|
||||
}
|
||||
html += '</table>';
|
||||
statusContainer.innerHTML = html;
|
||||
@@ -183,12 +183,12 @@
|
||||
detailRow.querySelector('td').innerHTML = '<div class="panel-empty">No detailed stats available for this period.</div>';
|
||||
}
|
||||
} catch (e) {
|
||||
detailRow.querySelector('td').innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed: ${e.message}</div>`;
|
||||
detailRow.querySelector('td').innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed: ${escapeHtml(e.message)}</div>`;
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
statusContainer.innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed to load health status: ${e.message}</div>`;
|
||||
statusContainer.innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed to load health status: ${escapeHtml(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,10 +209,10 @@
|
||||
for (const inc of open) {
|
||||
html += `<div style="padding: 10px 12px; margin-bottom: 8px; border: 1px solid var(--bad-fg)30; border-radius: 8px; background: var(--bad-bg);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span style="font-weight: 500;">${inc.serviceId}</span>
|
||||
<span style="font-weight: 500;">${escapeHtml(inc.serviceId)}</span>
|
||||
<span>${severityBadge(inc.severity)}</span>
|
||||
</div>
|
||||
<div style="font-size: 0.82rem; color: var(--muted); margin-top: 4px;">${inc.message}</div>
|
||||
<div style="font-size: 0.82rem; color: var(--muted); margin-top: 4px;">${escapeHtml(inc.message)}</div>
|
||||
<div style="font-size: 0.75rem; color: var(--muted); margin-top: 4px;">Started ${timeAgo(inc.createdAt)} · ${inc.occurrences || 1} occurrence(s)</div>
|
||||
</div>`;
|
||||
}
|
||||
@@ -231,8 +231,8 @@
|
||||
const resolved = inc.status === 'resolved';
|
||||
const dur = resolved && inc.duration ? (inc.duration < 60000 ? Math.round(inc.duration / 1000) + 's' : Math.round(inc.duration / 60000) + 'm') : '-';
|
||||
html += `<tr style="border-bottom: 1px solid var(--border);">`;
|
||||
html += `<td style="padding: 6px;">${inc.serviceId}</td>`;
|
||||
html += `<td style="padding: 6px;">${inc.type}</td>`;
|
||||
html += `<td style="padding: 6px;">${escapeHtml(inc.serviceId)}</td>`;
|
||||
html += `<td style="padding: 6px;">${escapeHtml(inc.type)}</td>`;
|
||||
html += `<td style="padding: 6px;">${severityBadge(inc.severity)}</td>`;
|
||||
html += `<td style="padding: 6px;"><span style="color: ${resolved ? 'var(--ok-fg)' : 'var(--bad-fg)'};">${inc.status}</span></td>`;
|
||||
html += `<td style="padding: 6px;">${dur}</td>`;
|
||||
@@ -244,7 +244,7 @@
|
||||
|
||||
incidentsContainer.innerHTML = html || '<div class="panel-empty"><span class="empty-icon">🚨</span>No incidents recorded yet.</div>';
|
||||
} catch (e) {
|
||||
incidentsContainer.innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed: ${e.message}</div>`;
|
||||
incidentsContainer.innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed: ${escapeHtml(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,18 +262,18 @@
|
||||
for (const s of services) {
|
||||
const isUp = s.status === 'up';
|
||||
html += `<tr style="border-bottom: 1px solid var(--border);">`;
|
||||
html += `<td style="padding: 8px; font-weight: 500;">${s.name || s.serviceId}</td>`;
|
||||
html += `<td style="padding: 8px; font-weight: 500;">${escapeHtml(s.name || s.serviceId)}</td>`;
|
||||
html += `<td style="padding: 8px; color: ${isUp ? 'var(--ok-fg)' : 'var(--bad-fg)'};">${isUp ? 'Up' : 'Down'}</td>`;
|
||||
html += `<td style="padding: 8px;">${s.sla?.target ? s.sla.target + '%' : '-'}</td>`;
|
||||
html += `<td style="padding: 8px; text-align: right;">`;
|
||||
html += `<button onclick="document.dispatchEvent(new CustomEvent('health-edit', {detail:'${s.serviceId}'}))" style="padding: 4px 10px; font-size: 0.78rem; margin-right: 4px;">Edit</button>`;
|
||||
html += `<button onclick="document.dispatchEvent(new CustomEvent('health-delete', {detail:'${s.serviceId}'}))" style="padding: 4px 10px; font-size: 0.78rem; color: var(--bad-fg); border-color: var(--bad-fg);">Delete</button>`;
|
||||
html += `<button onclick="document.dispatchEvent(new CustomEvent('health-edit', {detail:'${escapeHtml(s.serviceId)}'}))" style="padding: 4px 10px; font-size: 0.78rem; margin-right: 4px;">Edit</button>`;
|
||||
html += `<button onclick="document.dispatchEvent(new CustomEvent('health-delete', {detail:'${escapeHtml(s.serviceId)}'}))" style="padding: 4px 10px; font-size: 0.78rem; color: var(--bad-fg); border-color: var(--bad-fg);">Delete</button>`;
|
||||
html += '</td></tr>';
|
||||
}
|
||||
html += '</table>';
|
||||
configContainer.innerHTML = html;
|
||||
} catch (e) {
|
||||
configContainer.innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed: ${e.message}</div>`;
|
||||
configContainer.innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed: ${escapeHtml(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user