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