feat(security): apply html.escape sanitization across WebUI table rendering
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m35s

This commit is contained in:
2026-09-26 13:00:07 +02:00
parent 41cb956416
commit 93d9f0e463
+38 -22
View File
@@ -11,6 +11,7 @@ import secrets
import hashlib
import hmac
import time
import html
import warnings
import threading
from datetime import datetime, timezone, timedelta
@@ -1806,24 +1807,29 @@ def serve_webui(request: Request = None):
client_rows = []
for cl in clients:
cid, host, ost, fp, st, first, last = cl
esc_cid = html.escape(str(cid))
esc_host = html.escape(str(host))
esc_ost = html.escape(str(ost))
esc_fp = html.escape(str(fp))
esc_st = html.escape(str(st))
badge_cls = "badge-active" if st == "active" else "badge-revoked"
rel_time = format_relative_time(last)
last_display = f"<div><strong>{last[:19]}</strong></div><div style='font-size:11px; color:#94a3b8;'>{rel_time}</div>"
last_display = f"<div><strong>{html.escape(str(last[:19]))}</strong></div><div style='font-size:11px; color:#94a3b8;'>{html.escape(rel_time)}</div>"
btn_action = (
f'<button class="btn btn-outline" style="padding:4px 8px; font-size:11px;" onclick="revokeClient(\'{cid}\')">Revoke</button>'
f'<button class="btn btn-outline" style="padding:4px 8px; font-size:11px;" onclick="revokeClient(\'{esc_cid}\')">Revoke</button>'
if st == "active" else
f'<button class="btn btn-accent" style="padding:4px 8px; font-size:11px;" onclick="approveClient(\'{cid}\')">Approve</button>'
f'<button class="btn btn-accent" style="padding:4px 8px; font-size:11px;" onclick="approveClient(\'{esc_cid}\')">Approve</button>'
)
btn_del = f'<button class="btn btn-danger" style="padding:4px 8px; font-size:11px;" onclick="deleteClient(\'{cid}\')" title="Remove client and free seat">Remove</button>'
btn_del = f'<button class="btn btn-danger" style="padding:4px 8px; font-size:11px;" onclick="deleteClient(\'{esc_cid}\')" title="Remove client and free seat">Remove</button>'
client_rows.append(f"""
<tr>
<td><span class="badge {badge_cls}">{st.upper()}</span></td>
<td><strong>{host}</strong></td>
<td><code>{cid}</code></td>
<td><span class="badge badge-os">{ost.upper()}</span></td>
<td><code title="{fp}" style="cursor:pointer;" onclick="copyText('{fp}', 'Cert Fingerprint')">{fp[:16]}...</code></td>
<td><span class="badge {badge_cls}">{esc_st.upper()}</span></td>
<td><strong>{esc_host}</strong></td>
<td><code>{esc_cid}</code></td>
<td><span class="badge badge-os">{esc_ost.upper()}</span></td>
<td><code title="{esc_fp}" style="cursor:pointer;" onclick="copyText('{esc_fp}', 'Cert Fingerprint')">{esc_fp[:16]}...</code></td>
<td>{last_display}</td>
<td>
<div style="display:flex; gap:6px; align-items:center;">
@@ -1839,15 +1845,20 @@ def serve_webui(request: Request = None):
anomaly_rows = []
for an in anomalies:
afp, srv, sev, msg, rc, ls = an
esc_srv = html.escape(str(srv))
esc_sev = html.escape(str(sev))
esc_msg = html.escape(str(msg))
esc_ls = html.escape(str(ls[:19]))
sev_cls = "badge-error" if sev in ("ERROR", "CRITICAL", "FATAL") else "badge-warning"
short_msg = msg[:80] + "..." if len(msg) > 80 else msg
esc_short_msg = html.escape(str(short_msg))
anomaly_rows.append(f"""
<tr>
<td><span class="badge {sev_cls}">{sev}</span></td>
<td><strong>{srv}</strong></td>
<td><span class="badge badge-active">{rc} runs</span></td>
<td title="{msg}">{short_msg}</td>
<td>{ls[:19]}</td>
<td><span class="badge {sev_cls}">{esc_sev}</span></td>
<td><strong>{esc_srv}</strong></td>
<td><span class="badge badge-active">{int(rc)} runs</span></td>
<td title="{esc_msg}">{esc_short_msg}</td>
<td>{esc_ls}</td>
</tr>
""")
anomalies_html = "".join(anomaly_rows) if anomaly_rows else '<tr><td colspan="5" class="empty-state">No verified anomalies currently reported.</td></tr>'
@@ -1856,19 +1867,24 @@ def serve_webui(request: Request = None):
token_rows = []
for tk in hermes_tokens:
tok, tname, tcreated, tlast, tstatus = tk
esc_tok = html.escape(str(tok))
esc_tname = html.escape(str(tname))
esc_tstatus = html.escape(str(tstatus))
esc_tcreated = html.escape(str(tcreated[:19]))
esc_tlast = html.escape(str(tlast[:19])) if tlast else 'Never'
tbadge = "badge-active" if tstatus == "active" else "badge-revoked"
masked = tok[:10] + "••••••••" + tok[-4:]
tok_action = f'<button class="btn btn-danger" style="padding:4px 8px; font-size:11px;" onclick="revokeToken(\'{tok}\')">Revoke</button>' if tstatus == "active" else f'<button class="btn btn-outline" style="padding:4px 8px; font-size:11px;" onclick="deleteToken(\'{tok}\')">Delete</button>'
masked = esc_tok[:10] + "••••••••" + esc_tok[-4:]
tok_action = f'<button class="btn btn-danger" style="padding:4px 8px; font-size:11px;" onclick="revokeToken(\'{esc_tok}\')">Revoke</button>' if tstatus == "active" else f'<button class="btn btn-outline" style="padding:4px 8px; font-size:11px;" onclick="deleteToken(\'{esc_tok}\')">Delete</button>'
token_rows.append(f"""
<tr>
<td><strong>{tname}</strong></td>
<td><code style="cursor:pointer;" title="Click to copy full token" onclick="copyText('{tok}', 'Hermes API Token')">{masked}</code></td>
<td><span class="badge {tbadge}">{tstatus.upper()}</span></td>
<td style="color:#94a3b8; font-size:12px;">{tcreated[:19]}</td>
<td style="color:#94a3b8; font-size:12px;">{tlast[:19] if tlast else 'Never'}</td>
<td><strong>{esc_tname}</strong></td>
<td><code style="cursor:pointer;" title="Click to copy full token" onclick="copyText('{esc_tok}', 'Hermes API Token')">{masked}</code></td>
<td><span class="badge {tbadge}">{esc_tstatus.upper()}</span></td>
<td style="color:#94a3b8; font-size:12px;">{esc_tcreated}</td>
<td style="color:#94a3b8; font-size:12px;">{esc_tlast}</td>
<td>
<div style="display:flex; gap:6px; align-items:center;">
<button class="btn btn-secondary" style="padding:4px 8px; font-size:11px;" onclick="copyText('{tok}', 'Hermes API Token')">Copy</button>
<button class="btn btn-secondary" style="padding:4px 8px; font-size:11px;" onclick="copyText('{esc_tok}', 'Hermes API Token')">Copy</button>
{tok_action}
</div>
</td>