# unstructured: Server-Side Request Forgery in the URL-based partitioning

unstructured's partition(url=) fetches any URL with requests.get() and no host validation, returning the body as Element text — full-read SSRF to loopback and cloud metadata.

- Payout: Research
- Severity: critical
- Program: unstructured
- Found by: hayato1121
- Disclosed: 2026-09-03
- CVE: CVE-2026-71428
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-4mvj-m6j5-pmf7
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-4mvj-m6j5-pmf7
- Tags: area:ai domain:ai domain:oss impact:data-leak impact:internal-access kind:research platform:ghsa sev:critical tech:python technique:chained technique:source-review vuln:ssrf

**Target:** Python library URL ingestion (partition/partition_html/partition_md url= argument)
**Root cause:** The url= argument is passed straight to requests.get() with allow_redirects=True and no is_private/is_loopback/ipaddress checks in any of the three sinks; the fetched body is returned as Element text.
**Steps:**
1. Stand up a local Flask server on 127.0.0.1:9999 serving `/internal.html` (body `SK_LEAK_42`), `/imds` (fake credentials JSON) and `/redir` (302 to `http://127.0.0.1:9999/imds`).
2. Call `partition(url="http://127.0.0.1:9999/internal.html", languages=["eng"])` and observe `SK_LEAK_42` in the returned Element text.
3. Call `partition(url="http://127.0.0.1:9999/redir", languages=["eng"])` — the default `allow_redirects=True` follows to `/imds` and returns `SecretAccessKey`.
4. In production substitute `169.254.169.254` or `metadata.google.internal` for the loopback host.
**How found:** Source review of the three URL sinks (auto.py:303, html/partition.py:160, md.py:96) noting absence of is_private, is_loopback, ipaddress, gethostbyname and allow_redirects handling; git history showed no validation since 0.4.7 (~219 releases).
**Exploitation:** Attacker supplies a URL to any partition* entry point (directly or via LangChain UnstructuredURLLoader, LlamaIndex UnstructuredReader, Chainlit). The library GETs it server-side and returns the response body as parsed Element text, so the attacker reads internal HTTP services, IMDSv1/GCP/Azure metadata credentials, and side-effecting GET endpoints; redirects bypass naive host checks and error/timing differences enable port scanning.
**Escalation:** Redirect bypass (allow_redirects=True) defeats host allowlists; DNS rebinding (TOCTOU) defeats resolve-then-fetch checks, closeable only by socket-pinning.
**Insight:** For libraries that are the URL-ingestion layer under agent frameworks, audit every partition*/loader entry point for a url= kwarg: check whether the Content-Type validation happens after the request (it does here) and whether allow_redirects defaults to True, which turns a host allowlist into a redirect bypass.

