Skip to content

Fix stored XSS in iframe and blockquote media types#501

Open
scott2b wants to merge 1 commit into
masterfrom
fix-embed-xss
Open

Fix stored XSS in iframe and blockquote media types#501
scott2b wants to merge 1 commit into
masterfrom
fix-embed-xss

Conversation

@scott2b

@scott2b scott2b commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

Fixes the stored XSS reported in #500: the media url field flows unmodified into innerHTML in the iframe and blockquote media types, so markup stored in a storymap's JSON can execute arbitrary script in any viewer's browser — on the shared hosted origin, that means one user's storymap can attack another logged-in user's editor session.

Why not #500 as written

The url field is overloaded by design: the iframe and blockquote media types are only selected when the field contains pasted embed markup (MediaType.js substring-matches "iframe" / "blockquote" against the field). #500 treats the field as a bare URL (iframe.src = media_id) and as plain text (textContent), which closes the hole but breaks every existing storymap that uses a pasted embed code or quote.

Approach

New src/js/media/EmbedUtil.js parses the pasted markup inertly with DOMParser (no scripts execute, no handlers fire, no subresources load) and rebuilds clean DOM from an allowlist:

  • IFrame: extract the first <iframe>'s src, validate it resolves to http(s) (rejects javascript:, data:, etc.), and build a fresh iframe copying only presentation attributes (width, height, frameborder, allowfullscreen, allow, scrolling, title). Event handlers, srcdoc, and sibling tags are discarded. A bare URL that routes to the iframe type now also works. Invalid input shows the standard media load error.
  • Blockquote: rebuild the quote from an allowlist of formatting tags (blockquote, p, cite, em, …) with all attributes stripped except validated link hrefs; script/style/object/embedding tags are dropped with their contents; unknown tags are unwrapped keeping their text.

Existing storymaps with pasted embed codes and quotes keep rendering. No new runtime dependency — EmbedUtil is pure browser DOM code bundled like any other module.

Tests

Adds the project's first JS unit tests: 16 tests covering the XSS vectors (event handlers, javascript: URLs, script/img injection) and the legitimate embed cases, run with the Node built-in test runner + jsdom (dev-only deps). npm test is now wired to run them.

Also declares terser-webpack-plugin as a devDependency — webpack.common.js requires it directly, but newer webpack 5 releases no longer ship it transitively, so fresh installs couldn't build.

Verification

  • npm test: 16/16 pass
  • npx webpack -c webpack.dev.js: compiles successfully

Closes #500 (supersedes).

🤖 Generated with Claude Code

The media url field is overloaded: for the iframe and blockquote media
types it holds raw HTML pasted by the user, which was injected directly
via innerHTML. Any markup stored in a storymap's JSON could therefore
execute script in a viewer's browser (stored XSS on the shared hosted
origin).

Unlike PR #500, which treated the field as a bare URL and would have
broken every existing storymap with a pasted embed code, this fix
parses the pasted markup inertly with DOMParser and rebuilds clean DOM:

- IFrame: extract and validate the src (http/https only), copy only
  allowlisted presentation attributes onto a freshly created iframe.
  Bare URLs that route to the iframe type now also work. Invalid
  embeds show the standard media load error.
- Blockquote: rebuild the quote from an allowlist of formatting tags,
  stripping all attributes except validated link hrefs; script/object/
  style and similar tags are dropped with their contents.

Adds tests (node --test + jsdom, wired to npm test) and declares
terser-webpack-plugin, which webpack.common.js requires directly but
newer webpack 5 releases no longer provide transitively.

Co-Authored-By: Claude Opus 4.8 (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