fix: sanitize v2 HTML output to prevent stored XSS (GHSA-v5mq-3xhg-98m9)
The v2 (ontology) HTML path emitted untrusted document markup with no
output encoding, so attacker-controlled content (on* handlers,
javascript: links, tag/attribute breakout) survived into
elements_to_html() / metadata.text_as_html and executed when viewed.
- Add unstructured/documents/html_sanitization.py: shared tag/attribute
allowlists, URL-scheme filtering (data:image/* carve-out), and an
nh3-backed fragment sanitizer.
- ontology.to_html: HTML-escape text and attribute values, drop on*
and unsafe-scheme attributes, validate tag name against the allowlist.
- transformations: filter attributes at ingest (no escaping) so emit-time
escaping happens exactly once.
- elements_to_html: run assembled output through nh3 as defense-in-depth;
fix node-skipping bug when reinserting sanitized content.
- Add unit + end-to-end regression tests for all four vectors plus
legitimate-formatting preservation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>