Skip to content

Renderer: sanitize raw HTML in every Markdown Streamdown renders - #1177

Merged
ramnique merged 1 commit into
mainfrom
ramnique/sanitize-markdown-html
Oct 9, 2026
Merged

ramnique merged 1 commit into
mainfrom
ramnique/sanitize-markdown-html

Conversation

@ramnique

@ramnique ramnique commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Security fix. It affects the shipping desktop app.

The bug

Streamdown parses raw HTML in Markdown (rehype-raw) and only hardens URLs. Any Markdown the renderer shows through it could therefore carry live HTML: a Spaces message from another member, a model's reply (prompt injection from an email or web page), a meeting summary.

<iframe srcdoc="<script>…</script>"> runs its script in the renderer's own origin, where window.ipc is in reach. React blocks <img onerror> and javascript: links, but nothing blocks srcdoc.

Reproduced against a local Harbor: Arjun posted a message with that payload, and its script ran when another member's client rendered it.

The fix

  • components/streamdown.tsx wraps Streamdown with a fixed plugin list: rehype-raw, then rehype-sanitize with GitHub's allowlist, then KaTeX and harden as before. The list is set last, so no caller can turn the sanitizer off.
  • Kept: <br>, <details>, <sub>, tables and the like.
  • Dropped: scripts, frames, styles and event handlers.
  • Allowed on top of GitHub's list:
    • app://, which Spaces rewrites mentions, files and blobs to before parsing.
    • rowboat:// deep links.
    • blob: and data: images, which Streamdown already allowed.
    • remark-math's classes on <code>.
  • All nine call sites import the wrapper.
  • streamdown.test.tsx pins the attack payloads and the safe HTML, and fails if anything imports Streamdown from 'streamdown' directly.
  • New dependency: rehype-sanitize@6.0.0. The lockfile adds only it and hast-util-sanitize.

Verified

  • The renderer's full suite passes (141 files, 1157 tests) and tsc -b is clean.
  • In a browser (the Spaces web build against a local Harbor):
    • The srcdoc message renders empty and the page title is unchanged.
    • Math (KaTeX), fenced code, <br>, <sub> and <details> still render.
    • Mentions, space chips, files and images still render; the existing Spaces markdown tests cover these.
  • Not run in the packaged desktop app.

🤖 Generated with Claude Code

Streamdown parses raw HTML in Markdown (rehype-raw) and only hardens
URLs. A Spaces message, a model's reply or a meeting summary could
therefore carry <iframe srcdoc="<script>…">, and its script ran in the
renderer's own origin with window.ipc in reach. Reproduced against a
local Harbor: a message from another member ran script on render.
<img onerror> and javascript: links were already stopped by React; srcdoc
was not.

components/streamdown.tsx wraps Streamdown with a fixed plugin list:
rehype-raw, then rehype-sanitize with GitHub's allowlist, then KaTeX and
harden as before. The allowlist keeps <br>, <details>, <sub>, tables and
the like, and drops scripts, frames, styles and event handlers. It also
allows the renderer's own URL schemes: app://, which Spaces rewrites
mentions, files and blobs to, and rowboat:// deep links, plus blob: and
data: images, as Streamdown already allowed. remark-math's classes stay
on <code>.

All nine call sites import the wrapper. streamdown.test.tsx pins the
payloads and fails if anything imports Streamdown from 'streamdown'
directly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ramnique
ramnique merged commit 72e9d8c into main Oct 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant