From 763737110051880b6155cf976dba3f7e5ff87bc5 Mon Sep 17 00:00:00 2001
From: Kurtisone <104103601+Kurtisone@users.noreply.github.com>
Date: Wed, 12 Aug 2026 15:48:34 +0000
Subject: [PATCH 1/5] fix(ui): close stored XSS in the markdown renderer (audit
C-3)
escapeHtml() escaped only &, < and >, but its output lands in
attribute position -- inlineMarkdown built by direct
regex interpolation. `[x](" autofocus onfocus="location=name)`
therefore rendered as a valid with an injected event handler and
executed with no interaction. A `javascript:` href needed no quote at
all.
This matters beyond the model's own output: the text passed to
formatContent() includes tool results -- web_fetch/research page
content, files:read, sysadmin logs -- so a hostile page reached it.
And the API token lives in localStorage, so the payoff was token
theft, then authenticated /chat calls, then whatever shell/files are
enabled.
Three layers, in order of how much each actually closes:
- escapeHtml now escapes " and ' too (kills the attribute escape)
- link rendering validates the scheme against an allowlist, and
downgrades anything else to plain text (kills javascript:)
- GET / serves a CSP; 'unsafe-inline' is still required by the
single-file UI, so this doesn't stop injected script from running
-- but connect-src 'self' stops it shipping the token off-host
The two JS fixes are covered by source assertions rather than
execution: a regression tripwire, not a proof, and honest about it in
the test module docstring. The headers are tested for real.
---
src/forge/api.py | 33 +++++++++++++++--
src/forge/static/index.html | 32 +++++++++++++++--
tests/test_ui_security.py | 70 +++++++++++++++++++++++++++++++++++++
3 files changed, 131 insertions(+), 4 deletions(-)
create mode 100644 tests/test_ui_security.py
diff --git a/src/forge/api.py b/src/forge/api.py
index fe0179e..ee56314 100644
--- a/src/forge/api.py
+++ b/src/forge/api.py
@@ -434,11 +434,40 @@ async def compact():
# ─── UI ────────────────────────────────────────────────────────────
+# Third layer under the two fixes in static/index.html (quote escaping
+# + link-scheme validation): defense in depth, not a replacement for
+# them. 'unsafe-inline' is unavoidable for now -- the UI is a single
+# self-contained file with inline