Skip to content

fix: sanitize v2 HTML output to prevent stored XSS (GHSA-v5mq-3xhg-98m9) - #3

Open
anurag6569201 wants to merge 1 commit into
qa/agent-unstructured-io-unstructured/pr-03-4394/basefrom
qa/agent-unstructured-io-unstructured/pr-03-4394/head
Open

anurag6569201 wants to merge 1 commit into
qa/agent-unstructured-io-unstructured/pr-03-4394/basefrom
qa/agent-unstructured-io-unstructured/pr-03-4394/head

Conversation

@anurag6569201

Copy link
Copy Markdown

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_html and 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:">, and on* handlers — are now neutralized.

The fix layers output-encoding at the emitter, filtering at ingest, and a sanitizer sweep at the assembly boundary.

Changes

  • New unstructured/documents/html_sanitization.py — single source of truth for the policy: tag allowlist, attribute allowlist (drops all on* handlers), URL-scheme filter (is_safe_url) rejecting javascript:/vbscript:/non-image data: while preserving http/https/mailto/tel/relative and data:image/*, plus an nh3-backed sanitize_html_fragment.
  • ontology.py (OntologyElement.to_html) — root-cause fix: HTML-escape element text and attribute values (quote=True closes 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 makes text_as_html safe on its own. to_text now strips markup from the raw text rather than the newly-escaped HTML, preserving text extraction.
  • transformations.py — attribute handling at ingest now filters (drops on*/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 through nh3 as defense-in-depth, covering attributes injected outside the emitter (e.g. href from metadata.url). Also fixes a node-skipping bug when reinserting sanitized content.
  • Adds nh3 dependency; version bump to 0.24.1 with CHANGELOG entry.

Tests

  • New test_html_sanitization.py (unit) and test_xss_sanitization.py (end-to-end PoC from the advisory), asserting all four vectors render inert in both elements_to_html output and text_as_html, plus preservation of tables, headings, safe links, and base64 images.
  • Updated two existing ontology tests whose expectations encoded the pre-fix behavior — notably test_malformed_html, which previously asserted a live <script> tag in the output.

Acceptance criteria

  • PoC renders inertly — none of the four vectors execute
  • on* attributes stripped/neutralized
  • javascript:/data:/vbscript: schemes dropped; http/https/mailto/relative + data:image/* preserved
  • Element text and attribute values HTML-escaped
  • Attribute-value breakout impossible
  • Regression tests for all four vectors + legitimate-formatting preservation

🤖 Generated with Claude Code

Review in cubic

Source merge-base: 445c95735c4045057f51f399bc04c657751923bd
Source head: 73a7fecfff845d5f90c3084f43f79572111307c2

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