{"schema":"threatline.record/2","record":{"id":"CVE-2026-104250","aliases":["GHSA-v5mq-3xhg-98m9"],"summary":"unstructured: Stored XSS via unescaped HTML generation","details":"### Summary\n\nWhen `unstructured` parses an untrusted document and outputs the result as HTML, malicious markup contained in the document (e.g. `<img src=x onerror=...>`) is left in the output HTML without escaping. When that output is displayed in a browser, the script embedded in the document executes in the viewer's browser.\n\n### Details\n\n`partition_html(html_parser_version=\"v2\")` (and the VLM partitioner) preserve the input document's element tags, attributes, and text into `metadata.text_as_html`, and `elements_to_html()` outputs that as an HTML document.\n\nThe function that builds HTML on this path, `to_html()` in `unstructured/documents/ontology.py`, performs no escaping at all. As a result, event handlers such as `onerror` / `onmouseover` / `onload` originating from the input document are left in the output HTML as-is (literal `<script>` tags are stripped by the parser, but attribute-based vectors and `javascript:` URLs are not). In addition, link URLs are not scheme-filtered, so `<a href=\"javascript:...\">` is also emitted.\n\nRelevant source: `documents/ontology.py` (`to_html`, `_construct_attribute_string`, `_generate_final_html`); `partition/html/transformations.py` (`text_as_html = ontology_element.to_html()`); `partition/html/convert.py` (`elements_to_html`).\n\n### PoC\n\nEnvironment: `downloads.unstructured.io/unstructured-io/unstructured:latest`, `unstructured` 0.23.3.\n\n```python\nfrom unstructured.partition.html import partition_html\nfrom unstructured.partition.html.convert import elements_to_html\n\nmalicious = ('<body class=\"Document\">'\n             '<p class=\"Paragraph\" onmouseover=\"alert(1)\">hello'\n             '<img src=x onerror=\"alert(document.domain)\"></p>'\n             '<a class=\"Hyperlink\" href=\"javascript:alert(document.cookie)\">click me</a>'\n             '</body>')\nels = partition_html(text=malicious, html_parser_version=\"v2\")\nopen(\"xss_output.html\", \"w\").write(elements_to_html(list(els), no_group_by_page=True))\n```\n\n`elements_to_html()` output body — the unescaped vectors survive:\n\n```html\n<p class=\"NarrativeText\" id=\"...\" onmouseover=\"alert(1)\">\n  <p class=\"Paragraph\">hello</p>\n  <img class=\"Image\" onerror=\"alert(document.domain)\" src=\"x\"/>\n</p>\n<a class=\"UncategorizedText\" href=\"javascript:alert(document.cookie)\" id=\"...\">click me</a>\n```\n\nOpening `xss_output.html` in a browser fires `alert` automatically (via `<img onerror>`), with no interaction. Per-vector confirmation (each payload labelled, each verified in a browser):\n\n| Vector | Trigger | Cause |\n|---|---|---|\n| `<img onerror>` | open file (auto) | element `text` not escaped |\n| `<svg onload>` (via `title='\"><svg ...>'`) | open file (auto) | attribute value not escaped (`\"` breakout) |\n| `<a href=javascript:>` | click | link URL scheme not filtered |\n| `onmouseover` | hover | source attribute preserved as-is |\n\n### Impact\n\nStored XSS caused by improper output encoding. Anyone who renders the HTML or `text_as_html` generated from an untrusted document is affected. In an ingestion pipeline where uploaded documents are parsed and a staff member reviews their contents in a web UI, a low-privileged or external submitter can inject malicious HTML; when a privileged reviewer later opens the parsed result, `<img onerror>` fires in their session with no interaction, leading to cross-record information disclosure and privileged actions.","categories":["Libraries & packages"],"product":"unstructured","vendor":"Unstructured-IO","ecosystem":"pip","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-v5mq-3xhg-98m9","source_modified_at":"2026-10-15T20:16:20Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"pip","name":"unstructured"},"range":"<= 0.24.0"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-104250","source_modified_at":"2026-10-14T15:32:42.130Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"unstructured","vendor":"Unstructured-IO","versions":[{"version":"< 0.24.1","status":"affected"}],"defaultStatus":null},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-v5mq-3xhg-98m9","source_modified_at":"2026-10-15T20:30:08.640314887Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"PyPI","name":"unstructured","purl":"pkg:pypi/unstructured"},"ranges":[{"events":[{"introduced":"0"},{"fixed":"0.24.1"}],"type":"ECOSYSTEM"}],"versions":["0.0.1.dev0","0.10.0","0.10.1","0.10.10","0.10.11","0.10.12","0.10.13","0.10.14","0.10.15","0.10.16","0.10.18","0.10.19","0.10.19.dev18","0.10.2","0.10.20","0.10.21","0.10.22","0.10.23","0.10.24","0.10.25","0.10.26","0.10.27","0.10.28","0.10.29","0.10.30","0.10.4","0.10.5","0.10.6","0.10.7","0.10.8","0.10.9","0.11.0","0.11.1","0.11.2","0.11.4","0.11.5","0.11.6","0.11.7","0.11.8","0.12.0","0.12.2","0.12.3","0.12.4","0.12.5","0.12.6","0.13.0","0.13.1","0.13.2","0.13.3","0.13.4"]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-v5mq-3xhg-98m9","source_modified_at":"2026-10-15T20:16:20Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"pip","name":"unstructured"},"version":"0.24.1"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-v5mq-3xhg-98m9","source_modified_at":"2026-10-15T20:30:08.640314887Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"PyPI","name":"unstructured","purl":"pkg:pypi/unstructured"},"version":"0.24.1"}],"cvss":6.1,"severity":"Medium","kev":{"listed":false},"epss":{"probability":0.00194,"percentile":0.08584,"score_date":"2026-10-23"},"published_at":"2026-10-13T16:29:28.813Z","modified_at":"2026-10-15T20:30:08.640314887Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-v5mq-3xhg-98m9","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-15T20:16:20Z"},{"source":"CVE List","url":"https://www.cve.org/CVERecord?id=CVE-2026-104250","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-14T15:32:42.130Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-v5mq-3xhg-98m9","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-15T20:30:08.640314887Z"},{"source":"EPSS","url":"https://api.first.org/data/v1/epss?cve=CVE-2026-104250","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-23"}],"sources":["GitHub Advisory Database","CVE List","OSV","EPSS"],"references":[{"source":"CVE List","url":"https://github.com/Unstructured-IO/unstructured/security/advisories/GHSA-v5mq-3xhg-98m9","tags":["exploit"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-104250","source":"GitHub Advisory Database"},{"source":"CVE List","url":"https://github.com/Unstructured-IO/unstructured/pull/4394","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/Unstructured-IO/unstructured/commit/cda16b3f2170019be9c6ea5635946fb3eaa2d66f","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/Unstructured-IO/unstructured/releases/tag/0.24.1","tags":["x_refsource_MISC"]},{"url":"https://github.com/advisories/GHSA-v5mq-3xhg-98m9","source":"GitHub Advisory Database"}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"cve_state":"PUBLISHED","ssvc":{"exploitation":"poc","automatable":"no","technical_impact":"partial","assessed_at":"2026-10-14T15:32:11.460266Z"},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"ecosystem","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-15T20:30:08.640314887Z","content_hash":"ad44a92e42e32c9d5597f57279b4ebfb5fb5a42ea6a8319cee8caf5b360ddeec"}}
