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 hashlib
import hmac import hmac
import time import time
import html
import warnings import warnings
import threading import threading
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
@@ -1806,24 +1807,29 @@ def serve_webui(request: Request = None):
client_rows = [] client_rows = []
for cl in clients: for cl in clients:
cid, host, ost, fp, st, first, last = cl 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" badge_cls = "badge-active" if st == "active" else "badge-revoked"
rel_time = format_relative_time(last) 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 = ( 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 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""" client_rows.append(f"""
<tr> <tr>
<td><span class="badge {badge_cls}">{st.upper()}</span></td> <td><span class="badge {badge_cls}">{esc_st.upper()}</span></td>
<td><strong>{host}</strong></td> <td><strong>{esc_host}</strong></td>
<td><code>{cid}</code></td> <td><code>{esc_cid}</code></td>
<td><span class="badge badge-os">{ost.upper()}</span></td> <td><span class="badge badge-os">{esc_ost.upper()}</span></td>
<td><code title="{fp}" style="cursor:pointer;" onclick="copyText('{fp}', 'Cert Fingerprint')">{fp[:16]}...</code></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>{last_display}</td>
<td> <td>
<div style="display:flex; gap:6px; align-items:center;"> <div style="display:flex; gap:6px; align-items:center;">
@@ -1839,15 +1845,20 @@ def serve_webui(request: Request = None):
anomaly_rows = [] anomaly_rows = []
for an in anomalies: for an in anomalies:
afp, srv, sev, msg, rc, ls = an 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" sev_cls = "badge-error" if sev in ("ERROR", "CRITICAL", "FATAL") else "badge-warning"
short_msg = msg[:80] + "..." if len(msg) > 80 else msg short_msg = msg[:80] + "..." if len(msg) > 80 else msg
esc_short_msg = html.escape(str(short_msg))
anomaly_rows.append(f""" anomaly_rows.append(f"""
<tr> <tr>
<td><span class="badge {sev_cls}">{sev}</span></td> <td><span class="badge {sev_cls}">{esc_sev}</span></td>
<td><strong>{srv}</strong></td> <td><strong>{esc_srv}</strong></td>
<td><span class="badge badge-active">{rc} runs</span></td> <td><span class="badge badge-active">{int(rc)} runs</span></td>
<td title="{msg}">{short_msg}</td> <td title="{esc_msg}">{esc_short_msg}</td>
<td>{ls[:19]}</td> <td>{esc_ls}</td>
</tr> </tr>
""") """)
anomalies_html = "".join(anomaly_rows) if anomaly_rows else '<tr><td colspan="5" class="empty-state">No verified anomalies currently reported.</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 = [] token_rows = []
for tk in hermes_tokens: for tk in hermes_tokens:
tok, tname, tcreated, tlast, tstatus = tk 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" tbadge = "badge-active" if tstatus == "active" else "badge-revoked"
masked = tok[:10] + "••••••••" + tok[-4:] masked = esc_tok[:10] + "••••••••" + esc_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>' 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""" token_rows.append(f"""
<tr> <tr>
<td><strong>{tname}</strong></td> <td><strong>{esc_tname}</strong></td>
<td><code style="cursor:pointer;" title="Click to copy full token" onclick="copyText('{tok}', 'Hermes API Token')">{masked}</code></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}">{tstatus.upper()}</span></td> <td><span class="badge {tbadge}">{esc_tstatus.upper()}</span></td>
<td style="color:#94a3b8; font-size:12px;">{tcreated[:19]}</td> <td style="color:#94a3b8; font-size:12px;">{esc_tcreated}</td>
<td style="color:#94a3b8; font-size:12px;">{tlast[:19] if tlast else 'Never'}</td> <td style="color:#94a3b8; font-size:12px;">{esc_tlast}</td>
<td> <td>
<div style="display:flex; gap:6px; align-items:center;"> <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} {tok_action}
</div> </div>
</td> </td>