Skip to content

Sanitize extractor HTML#326

Merged
kepano merged 2 commits into
mainfrom
fix/extractor-xss-GHSA-jg4p-g6xj-4qmf
Jun 24, 2026
Merged

Sanitize extractor HTML#326
kepano merged 2 commits into
mainfrom
fix/extractor-xss-GHSA-jg4p-g6xj-4qmf

Conversation

@kepano

@kepano kepano commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Fixes a stored XSS in site extractors. Several extractors build their output by interpolating attacker-controlled DOM attribute values into template-literal HTML strings without escaping:

// x-article.ts — `alt` read straight off the page
return `<img src="${this.upgradeImageSrc(src)}" alt="${alt}">`;

An alt (or src, og:image, video description) containing a " closes the attribute and the remainder becomes new attributes — e.g. alt='x" onerror="alert(1)' yields a live onerror handler. This output is returned through buildExtractorResponse(), which previously only resolved URLs and bypassed the DOM-based sanitization the main pipeline applies.

Fix

  1. Central sanitizer. All extractor output now flows through _sanitizeExtractorHtml(), which parses into a DOM and runs the same _stripUnsafeElements() pass used elsewhere (and resolves relative URLs in the same parse, so output is parsed/serialized once). This covers every extractor, present and future.
  2. Escape at the sink. escapeHtml() wraps the interpolated values in the three confirmed extractors. This is complementary, not redundant: the central pass strips on*/dangerous URLs but not injected style/class attributes, and escaping preserves the original text's integrity.

kepano added 2 commits June 23, 2026 18:56
Site extractors build their output from template-literal strings,
interpolating attacker-controlled DOM attribute values (image alt/src,
og:image, video description) without escaping. That output is returned
through buildExtractorResponse() which previously only resolved URLs,
bypassing the DOM-based sanitization the main pipeline applies — so an
injected event handler or javascript: URL could reach consumers that
render contentHtml.

Fix, defense in depth:
- Route all extractor output through _sanitizeExtractorHtml(), which
  parses into a DOM and runs the same _stripUnsafeElements() pass used
  elsewhere (and resolves relative URLs in the same pass). This covers
  every extractor, present and future.
- escapeHtml() the interpolated values at the confirmed sinks in
  x-article, substack, and youtube extractors. This is complementary,
  not redundant: it also blocks injection of attributes the central
  pass does not strip (style/class/etc.) and preserves data integrity.

Add a regression test that re-parses extractor output and asserts no
event-handler attributes or javascript: URLs survive.
@kepano
kepano merged commit baf2eae into main Jun 24, 2026
5 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