{"id":"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. `\u003cimg src=x onerror=...\u003e`) 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 `\u003cscript\u003e` tags are stripped by the parser, but attribute-based vectors and `javascript:` URLs are not). In addition, link URLs are not scheme-filtered, so `\u003ca href=\"javascript:...\"\u003e` 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 = ('\u003cbody class=\"Document\"\u003e'\n             '\u003cp class=\"Paragraph\" onmouseover=\"alert(1)\"\u003ehello'\n             '\u003cimg src=x onerror=\"alert(document.domain)\"\u003e\u003c/p\u003e'\n             '\u003ca class=\"Hyperlink\" href=\"javascript:alert(document.cookie)\"\u003eclick me\u003c/a\u003e'\n             '\u003c/body\u003e')\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\u003cp class=\"NarrativeText\" id=\"...\" onmouseover=\"alert(1)\"\u003e\n  \u003cp class=\"Paragraph\"\u003ehello\u003c/p\u003e\n  \u003cimg class=\"Image\" onerror=\"alert(document.domain)\" src=\"x\"/\u003e\n\u003c/p\u003e\n\u003ca class=\"UncategorizedText\" href=\"javascript:alert(document.cookie)\" id=\"...\"\u003eclick me\u003c/a\u003e\n```\n\nOpening `xss_output.html` in a browser fires `alert` automatically (via `\u003cimg onerror\u003e`), with no interaction. Per-vector confirmation (each payload labelled, each verified in a browser):\n\n| Vector | Trigger | Cause |\n|---|---|---|\n| `\u003cimg onerror\u003e` | open file (auto) | element `text` not escaped |\n| `\u003csvg onload\u003e` (via `title='\"\u003e\u003csvg ...\u003e'`) | open file (auto) | attribute value not escaped (`\"` breakout) |\n| `\u003ca href=javascript:\u003e` | 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, `\u003cimg onerror\u003e` fires in their session with no interaction, leading to cross-record information disclosure and privileged actions.","aliases":["CVE-2026-104250"],"modified":"2026-10-15T20:30:08.640314887Z","published":"2026-10-15T20:16:18Z","database_specific":{"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-10-15T20:16:18Z","nvd_published_at":"2026-10-13T17:17:15Z","cwe_ids":["CWE-116","CWE-79"]},"references":[{"type":"WEB","url":"https://github.com/Unstructured-IO/unstructured/security/advisories/GHSA-v5mq-3xhg-98m9"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-104250"},{"type":"WEB","url":"https://github.com/Unstructured-IO/unstructured/pull/4394"},{"type":"WEB","url":"https://github.com/Unstructured-IO/unstructured/commit/cda16b3f2170019be9c6ea5635946fb3eaa2d66f"},{"type":"PACKAGE","url":"https://github.com/Unstructured-IO/unstructured"},{"type":"WEB","url":"https://github.com/Unstructured-IO/unstructured/releases/tag/0.24.1"}],"affected":[{"package":{"name":"unstructured","ecosystem":"PyPI","purl":"pkg:pypi/unstructured"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.24.1"}]}],"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","0.13.5","0.13.6","0.13.7","0.14.0","0.14.10","0.14.2","0.14.2.dev1","0.14.3","0.14.4","0.14.5","0.14.6","0.14.7","0.14.8","0.14.9","0.15.0","0.15.1","0.15.10","0.15.12","0.15.13","0.15.14","0.15.3","0.15.5","0.15.6","0.15.7","0.15.8","0.15.9","0.16.0","0.16.1","0.16.10","0.16.11","0.16.12","0.16.13","0.16.14","0.16.15","0.16.16","0.16.17","0.16.19","0.16.2","0.16.20","0.16.21","0.16.22","0.16.23","0.16.24","0.16.25","0.16.3","0.16.4","0.16.5","0.16.6","0.16.7","0.16.8","0.16.9","0.17.0","0.17.2","0.18.1","0.18.11","0.18.13","0.18.14","0.18.15","0.18.18","0.18.2","0.18.20","0.18.21","0.18.24","0.18.26","0.18.27","0.18.3","0.18.31","0.18.32","0.18.5","0.18.6","0.18.7","0.18.9","0.2.0","0.2.1","0.2.2","0.2.3","0.2.4","0.2.5","0.2.6.dev1","0.20.2","0.20.6","0.20.8","0.21.0","0.21.1","0.21.2","0.21.5","0.22.10","0.22.12","0.22.16","0.22.18","0.22.20","0.22.21","0.22.22","0.22.23","0.22.26","0.22.27","0.22.28","0.22.29","0.22.30","0.22.31","0.22.32","0.22.6","0.23.0","0.23.1","0.24.0","0.3.0","0.3.1","0.3.2","0.3.3","0.3.4","0.3.5","0.4.0","0.4.1","0.4.10","0.4.11","0.4.12","0.4.13","0.4.14","0.4.15","0.4.16","0.4.2","0.4.3","0.4.4","0.4.6","0.4.7","0.4.8","0.4.9","0.5.0","0.5.1","0.5.10","0.5.11","0.5.12","0.5.13","0.5.2","0.5.3","0.5.4","0.5.6","0.5.7","0.5.8","0.5.9","0.6.0","0.6.1","0.6.10","0.6.11","0.6.2","0.6.3","0.6.4","0.6.5","0.6.6","0.6.7","0.6.8","0.6.9","0.7.0","0.7.1","0.7.10","0.7.11","0.7.12","0.7.2","0.7.3","0.7.4","0.7.5","0.7.6","0.7.7","0.7.8","0.7.9","0.8.0","0.8.1","0.8.3","0.8.4","0.8.5","0.8.6","0.8.7","0.8.8","0.9.0","0.9.1","0.9.2","0.9.3"],"database_specific":{"last_known_affected_version_range":"\u003c= 0.24.0","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-v5mq-3xhg-98m9/GHSA-v5mq-3xhg-98m9.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"}]}