~ / search / GHSA-v5mq-3xhg-98m9

GHSA-v5mq-3xhg-98m9

unstructured: Stored XSS via unescaped HTML generation

Source: GitHub Advisory Database (reviewed) Moderate CVE-2026-104250

Packages

EcosystemPackageAffectedFixed 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.