Skip to content

Sanitize every HTML sink of the frontend, starting with the announce fetched from GitHub - #58

Open
lgnap wants to merge 1 commit into
antoinevalentinHA:masterfrom
lgnap:fix/issue-12-frontend-markdown-xss
Open

lgnap wants to merge 1 commit into
antoinevalentinHA:masterfrom
lgnap:fix/issue-12-frontend-markdown-xss

Conversation

@lgnap

@lgnap lgnap commented Sep 11, 2026

Copy link
Copy Markdown

What

App.js rendered the announce — Markdown fetched from the upstream repository's ANNOUNCE.md through raw.githubusercontent.com — with dangerouslySetInnerHTML={{__html: marked(...)}}, through marked@1.x (EOL, known XSS issues before 2.0) and with no sanitization. Whoever could change that file, or sit between the user and GitHub, could run script in every STUdio user's browser. Four more sinks (PackLibrary.js ×2, PackDiagramWidget.js ×2) injected HTML from the translation bundles the same way.

All raw HTML now goes through one helper, src/utils/html.js:

  • renderMarkdown(md) — the announce
  • sanitizeHtml(html) — the translated dialogs

Both run DOMPurify. It keeps the markup the dialogs rely on (<p>, <ul>/<li>, <strong>, <em>, links, the <span class="glyphicon …"> of the help pages — inventoried from the locale files) and drops scripts, event handlers and javascript: URLs. Form controls are forbidden on top: a form in a dialog fetched from the network is a phishing prompt, not content.

Dependencies

  • marked ^1.0.0 → ^4.3.0, the last line that still supports Node 12 (what CI and web-ui/pom.xml pin).
  • dompurify ^3.4.15 added (no engine constraint; browser code).

yarn.lock diff is exactly those entries. Lockfile regenerated with yarn 1.22 — no collateral churn.

One wart, documented in the code: marked is imported as marked/lib/marked.umd.js rather than marked. marked 4's main is a .cjs file, and the jest of react-scripts 3 hands anything that isn't .js/.jsx/.ts/.tsx/.css/.json to its catch-all file transform, which turns it into a filename string; the fix (moduleNameMapper / transform) is one of the options CRA 3.0.1 refuses to let you override without ejecting. The UMD file is the same source, plain .js, and loads under both webpack 4 and jest 24. It goes away with a react-scripts upgrade, which is out of scope here.

Tests

html.test.js (9 tests) pins both directions — a sanitizer that also stripped the dialogs' markup would pass every "no script" assertion while breaking the help pages:

  • announce Markdown renders (heading, bold, link, list)
  • <script>, onerror/onclick, javascript: (Markdown link and HTML), <iframe>, <form>/<input> are removed
  • the exact markup used by the translation bundles is preserved byte for byte
  • null/undefined render as empty rather than throwing

Locally: yarn test → 66 passed (57 + 9); yarn build compiles; Java suite untouched and green. git diff --exit-code clean.

Tracked in lgnap#12.

🤖 Generated with Claude Code

…unce fetched from GitHub

App.js rendered the announce — Markdown fetched from the upstream
repository's ANNOUNCE.md through raw.githubusercontent.com — with
`dangerouslySetInnerHTML={{__html: marked(...)}}`, through marked 1.x
(end of life, known XSS issues before 2.0) and with no sanitization.
Whoever could change that file, or sit between the user and GitHub,
could run script in every STUdio user's browser. Four more sinks
injected HTML from the translation bundles the same way.

All raw HTML now goes through one helper, utils/html.js:
renderMarkdown() for the announce, sanitizeHtml() for the translated
dialogs. Both run DOMPurify, which keeps the markup the dialogs rely on
(paragraphs, lists, emphasis, links, the glyphicon spans) and drops
scripts, event handlers and javascript: URLs; form controls are
forbidden on top, since a form in a dialog fetched from the network is
a phishing prompt rather than content.

marked goes to 4.3.0, the last line that still supports the Node 12 CI
uses. It is imported by its UMD build: its `main` is a .cjs file that
the jest of react-scripts 3 hands to its catch-all file transform, and
that config cannot be overridden without ejecting.

html.test.js pins both directions: XSS payloads are removed, and the
markup the announce and the translations actually use survives.

Closes #12

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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