fix: sanitize v2 HTML output to prevent stored XSS (GHSA-v5mq-3xhg-98m9) - #3
Open
anurag6569201 wants to merge 1 commit into
Conversation
Source PR: Unstructured-IO#4394 Source head: 73a7fec
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The v2 (ontology) HTML path emitted untrusted document markup with no output encoding, so attacker-controlled content in a parsed document survived into
elements_to_html()/metadata.text_as_htmland executed when the output was viewed in a browser (stored XSS, GHSA-v5mq-3xhg-98m9). All four reported vectors —<img onerror>,<svg onload>(attribute-value breakout),<a href="javascript:">, andon*handlers — are now neutralized.The fix layers output-encoding at the emitter, filtering at ingest, and a sanitizer sweep at the assembly boundary.
Changes
unstructured/documents/html_sanitization.py— single source of truth for the policy: tag allowlist, attribute allowlist (drops allon*handlers), URL-scheme filter (is_safe_url) rejectingjavascript:/vbscript:/non-imagedata:while preservinghttp/https/mailto/tel/relative anddata:image/*, plus annh3-backedsanitize_html_fragment.ontology.py(OntologyElement.to_html) — root-cause fix: HTML-escape element text and attribute values (quote=Truecloses the attribute-value breakout), drop unsafe attributes, and validate the tag name against the allowlist (non-allowlisted tags like<script>fall back to inert<span>). This makestext_as_htmlsafe on its own.to_textnow strips markup from the raw text rather than the newly-escaped HTML, preserving text extraction.transformations.py— attribute handling at ingest now filters (dropson*/unsafe schemes) instead of escaping, so escaping happens exactly once at emit (no double-encoding). Backwards-compatible alias retained.convert.py(elements_to_html) — runs assembled output throughnh3as defense-in-depth, covering attributes injected outside the emitter (e.g.hreffrommetadata.url). Also fixes a node-skipping bug when reinserting sanitized content.nh3dependency; version bump to0.24.1with CHANGELOG entry.Tests
test_html_sanitization.py(unit) andtest_xss_sanitization.py(end-to-end PoC from the advisory), asserting all four vectors render inert in bothelements_to_htmloutput andtext_as_html, plus preservation of tables, headings, safe links, and base64 images.test_malformed_html, which previously asserted a live<script>tag in the output.Acceptance criteria
on*attributes stripped/neutralizedjavascript:/data:/vbscript:schemes dropped;http/https/mailto/relative +data:image/*preserved🤖 Generated with Claude Code
Source merge-base:
445c95735c4045057f51f399bc04c657751923bdSource head:
73a7fecfff845d5f90c3084f43f79572111307c2