AppSec · Aug 3, 2026 · 7 min read

HTTP request smuggling: confirming desync without poisoning shared state

A CL.TE or TE.CL desync is reachable when the server pair disagrees on where the request body ends - confirmed when a probe poisons a subsequent clean request with a measurable and benign artifact.

HTTP request smuggling exploits disagreement between a reverse proxy and a back-end server about where one HTTP request ends and the next begins. That disagreement lets an attacker prepend bytes to the next user's request, poison the back-end's queue, or bypass access controls enforced only at the edge. The confirming artifact that decides severity is not which server disagrees, but whether a controlled probe can demonstrate the poisoned prefix in a response you trigger yourself - without touching any other session.

01The desync mechanism: two rules, one request

HTTP/1.1 specifies two legal ways to delimit a request body: Content-Length gives the byte count; Transfer-Encoding: chunked uses chunk-size markers in the body. When a reverse proxy and a back-end server each pick one rule and disagree, a single TCP stream contains what each server considers the first request - and what the other considers the beginning of the next one. The leftover bytes sit in the back-end's read buffer, prepended to whatever connection comes in next.

CL.TE variant: front-end reads by Content-Length, back-end reads chunked Front-end sees: Back-end sees: POST /search HTTP/1.1 POST /search HTTP/1.1 Content-Length: 49 Transfer-Encoding: chunked Transfer-Encoding: chunked body: 49 bytes (forwarded as-is) 3c (chunk size) q=hello&x= (chunk data, 3c bytes) 0 (zero chunk: end of body) ➤ leftover bytes queued for next req
CL.TE: the front-end forwards 49 bytes; the back-end ends the body at the zero chunk, leaving the tail in the queue.

02Confirming the window without poisoning real traffic

A timing oracle is the first safe step. A CL.TE desync probe sets Content-Length to a value larger than the TE-encoded body, so the back-end waits for bytes that never arrive. A response that takes 10 seconds longer than the baseline - measured across several repeats and compared to a non-chunked control - is in-band evidence the back-end is blocking on chunked parsing, not Content-Length. No other session is touched; the delayed response affects only the probe connection.

smuggle-timing.httphttp
# CL.TE timing probe - back-end blocks on Transfer-Encoding
POST /api/search HTTP/1.1
Host: target.example
Content-Length: 6       # front-end forwards 6 bytes
Transfer-Encoding: chunked

0                        # zero chunk: end of TE body
X                        # 1 extra byte - back-end waits for more

# Baseline (same path, no TE header): immediate response
# Probe: response delayed ~10s  ->  back-end interprets TE, desync confirmed reachable

Escalation from reachable to confirmed requires a response-poisoning probe you control end to end. Immediately after the smuggling request, send a benign second request on the same or a fresh connection. If the back-end's reply to that second request reflects the smuggled prefix - an unexpected 404 for the poisoned path, a response that opens with bytes from the injected fragment, or an error naming the partial method from the prepended bytes - the desync is confirmed. The nonce in the injected prefix ensures the artifact is unique to this run, not coincidental.

Proven - HighCL.TE desync - response poisoning - reverse proxy at api.example
Req 1 (smuggle): POST /api/search CL:49 TE:chunked body ends with partial fragment ──► 200 OK (immediate; front-end accepted full CL) Req 2 (confirm): GET /api/profile (clean, no injection) ──► 404 Not Found body: "Cannot GET /NONCE-62615533-injected-path" Injected prefix prepended to Req 2 - back-end routed on poisoned URI.
Oracle: response-text differential. Nonce in prefix ensures uniqueness. No real user connection targeted. Severity high - confirmed queue poisoning on the shared back-end; escalates to critical if a tested path reaches an admin or auth-bypass endpoint.

03HTTP/2 downgrade and per-server behavior

HTTP/2 eliminates the CL-vs-TE ambiguity in the front-end-to-proxy leg because frames carry explicit length. But most reverse proxies translate H/2 to HTTP/1.1 before forwarding, and that translation reintroduces the ambiguity at the proxy-to-back-end hop. An H/2 request that carries an explicit Content-Length pseudo-header and a chunked body reaches the back-end as a normal HTTP/1.1 ambiguity. Nginx, HAProxy, Apache Traffic Server and AWS ALB each handle the downgrade differently; a probe that passes against one combination is silent against another.

Front-endBack-endDesync variantObfuscation needed
Nginx (H/1.1)Gunicorn / uWSGITE.CLTransfer-Encoding: chunked\x0d (CR obfuscation)
HAProxyNginxCL.TENone; default config is vulnerable
AWS ALB (H/2)Express (H/1.1)H2.CLContent-Length mismatch in H/2 pseudo-header
Cloudflare (H/2)Django (H/1.1)H2.TETE header injected via H/2 trailer
Apache Traffic ServerTomcatTE.TETransfer-Encoding: xchunked (non-standard value)

Probing TE.TE variants requires obfuscated TE headers because both servers technically support chunked encoding - the goal is to make one of them ignore the header and fall back to Content-Length. Common obfuscations include extra whitespace (Transfer-Encoding : chunked), non-standard chunk extension values, and CR/LF injection inside the header value. A scanner that probes only the canonical CL.TE and TE.CL payloads misses the TE.TE surface entirely and produces a silent false negative for every server pair in that row.

04Severity follows the confirmed impact, not the variant

The desync mechanism is the means, not the measure. Severity is set by what the confirmed poisoning can achieve on this target. A queue-poisoning path that causes the next request to receive a 404 for the injected route is confirmed high - real request-queue manipulation, demonstrated. The same mechanism that can prepend a full HTTP method and reach a path only accessible from the internal network is critical, because the access control that lives at the reverse proxy no longer holds. A timing oracle with no confirmed poisoning is rated reachable: the structural gap is real, the impact is not yet measured.

5
TE obfuscation variants probed per server pair
0
real user sessions touched during confirmation
1
nonce-bearing prefix per probe run

The fix is to enforce a single, unambiguous protocol interpretation at every hop. Reject or normalize any request that carries both Content-Length and Transfer-Encoding headers at the reverse proxy before forwarding. For HTTP/2 front-ends, validate that no Content-Length pseudo-header disagrees with the frame length before downgrading to HTTP/1.1. Where the infrastructure supports it, end-to-end HTTP/2 between proxy and back-end eliminates the CL/TE surface entirely. Each finding carries the exact smuggling payload, the back-end target version, and the confirming response - so the engineering team fixes the right header at the right hop, not the symptom downstream.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →