{"id":"CVE-2026-104250","aliases":["EUVD-2026-98918","GHSA-v5mq-3xhg-98m9"],"status":"published","title":"unstructured: Stored XSS via unescaped HTML generation","description":"The unstructured library provides open-source components for ingesting and pre-processing images and text documents, such as PDFs, HTML, Word docs, and many more. Prior to 0.24.1, partition_html(html_parser_version=\"v2\") and the VLM partitioner can preserve untrusted document tags, attributes, text, and link targets in metadata.text_as_html, while elements_to_html() emits the result as an HTML document. The to_html() path in unstructured/documents/ontology.py does not escape output or reject unsafe URL schemes, allowing event-handler attributes, attribute breakout markup, and javascript: links to survive parsing. When the generated HTML or text_as_html is rendered in a browser, attacker-controlled script can execute in the viewer's origin, including in document-ingestion workflows where a privileged reviewer opens submitted content. This issue is fixed in version 0.24.1.","severity":{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-12","score":0.00234,"percentile":0.14491,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-116","CWE-79"],"published":"2026-10-13T16:29:28Z","modified":"2026-11-11T10:47:23Z","fixes":[{"ecosystem":"PyPI","package":"unstructured","fixed_in":["0.24.1"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"unstructured","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"0.24.0"},{"fixed":"0.24.1"}]}]},{"source":"osv","ecosystem":"PyPI","package":"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"]}],"products":[{"vendor":"unstructured-io","product":"unstructured"}],"references":[{"url":"https://github.com/Unstructured-IO/unstructured/security/advisories/GHSA-v5mq-3xhg-98m9","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/Unstructured-IO/unstructured/pull/4394","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/Unstructured-IO/unstructured/commit/cda16b3f2170019be9c6ea5635946fb3eaa2d66f","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/Unstructured-IO/unstructured/releases/tag/0.24.1","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-104250","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-v5mq-3xhg-98m9"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/104xxx/CVE-2026-104250.json","tags":["advisory"]},{"url":"https://github.com/Unstructured-IO/unstructured","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-104250","title":"unstructured: Stored XSS via unescaped HTML generation","description":"The unstructured library provides open-source components for ingesting and pre-processing images and text documents, such as PDFs, HTML, Word docs, and many more. Prior to 0.24.1, partition_html(html_parser_version=\"v2\") and the VLM partitioner can preserve untrusted document tags, attributes, text, and link targets in metadata.text_as_html, while elements_to_html() emits the result as an HTML document. The to_html() path in unstructured/documents/ontology.py does not escape output or reject unsafe URL schemes, allowing event-handler attributes, attribute breakout markup, and javascript: links to survive parsing. When the generated HTML or text_as_html is rendered in a browser, attacker-controlled script can execute in the viewer's origin, including in document-ingestion workflows where a privileged reviewer opens submitted content. This issue is fixed in version 0.24.1.","status":"published","published":"2026-10-13T16:29:28.813Z","modified":"2026-10-14T15:32:42.13Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-79","CWE-116"],"updated_at":"2026-11-02T14:20:09.098623Z"}],"euvd":[{"id":"EUVD-2026-98918","description":"The unstructured library provides open-source components for ingesting and pre-processing images and text documents, such as PDFs, HTML, Word docs, and many more. Prior to 0.24.1, partition_html(html_parser_version=\"v2\") and the VLM partitioner can preserve untrusted document tags, attributes, text, and link targets in metadata.text_as_html, while elements_to_html() emits the result as an HTML document. The to_html() path in unstructured/documents/ontology.py does not escape output or reject unsafe URL schemes, allowing event-handler attributes, attribute breakout markup, and javascript: links to survive parsing. When the generated HTML or text_as_html is rendered in a browser, attacker-controlled script can execute in the viewer's origin, including in document-ingestion workflows where a privileged reviewer opens submitted content. This issue is fixed in version 0.24.1.","status":"published","published":"2026-10-13T16:29:28Z","modified":"2026-10-14T15:32:42Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T17:27:14.734034Z"}],"ghsa":[{"id":"GHSA-v5mq-3xhg-98m9","title":"unstructured: Stored XSS via unescaped HTML generation","description":"### 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.","status":"published","published":"2026-10-15T20:16:18Z","modified":"2026-10-15T20:16:20Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-79","CWE-116"],"updated_at":"2026-11-02T14:41:32.483482Z"}],"nvd":[{"id":"CVE-2026-104250","description":"The unstructured library provides open-source components for ingesting and pre-processing images and text documents, such as PDFs, HTML, Word docs, and many more. Prior to 0.24.1, partition_html(html_parser_version=\"v2\") and the VLM partitioner can preserve untrusted document tags, attributes, text, and link targets in metadata.text_as_html, while elements_to_html() emits the result as an HTML document. The to_html() path in unstructured/documents/ontology.py does not escape output or reject unsafe URL schemes, allowing event-handler attributes, attribute breakout markup, and javascript: links to survive parsing. When the generated HTML or text_as_html is rendered in a browser, attacker-controlled script can execute in the viewer's origin, including in document-ingestion workflows where a privileged reviewer opens submitted content. This issue is fixed in version 0.24.1.","status":"published","published":"2026-10-13T17:17:15.9Z","modified":"2026-10-16T16:00:33.533Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-79","CWE-116"],"updated_at":"2026-11-02T14:12:57.132005Z"}],"osv":[{"id":"CVE-2026-104250","title":"unstructured: Stored XSS via unescaped HTML generation","description":"The unstructured library provides open-source components for ingesting and pre-processing images and text documents, such as PDFs, HTML, Word docs, and many more. Prior to 0.24.1, partition_html(html_parser_version=\"v2\") and the VLM partitioner can preserve untrusted document tags, attributes, text, and link targets in metadata.text_as_html, while elements_to_html() emits the result as an HTML document. The to_html() path in unstructured/documents/ontology.py does not escape output or reject unsafe URL schemes, allowing event-handler attributes, attribute breakout markup, and javascript: links to survive parsing. When the generated HTML or text_as_html is rendered in a browser, attacker-controlled script can execute in the viewer's origin, including in document-ingestion workflows where a privileged reviewer opens submitted content. This issue is fixed in version 0.24.1.","status":"published","published":"2026-10-13T16:29:28.813Z","modified":"2026-11-11T10:47:23.199279027Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-116","CWE-79"],"updated_at":"2026-11-11T11:06:38.226849Z"},{"id":"GHSA-v5mq-3xhg-98m9","title":"unstructured: Stored XSS via unescaped HTML generation","description":"### 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.","status":"published","published":"2026-10-15T20:16:18Z","modified":"2026-10-15T20:30:08.640314887Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-116","CWE-79"],"updated_at":"2026-11-02T15:46:56.639206Z"}]}}
