GHSA-V5MQ-3XHG-98M9

Vulnerability from github – Published: 2026-10-15 20:16 – Updated: 2026-10-15 20:16
VLAI
Summary
unstructured: Stored XSS via unescaped HTML generation
Details

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.

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.

Show details on source website

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    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.