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.
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.
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.
# 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.
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-end | Back-end | Desync variant | Obfuscation needed |
|---|---|---|---|
| Nginx (H/1.1) | Gunicorn / uWSGI | TE.CL | Transfer-Encoding: chunked\x0d (CR obfuscation) |
| HAProxy | Nginx | CL.TE | None; default config is vulnerable |
| AWS ALB (H/2) | Express (H/1.1) | H2.CL | Content-Length mismatch in H/2 pseudo-header |
| Cloudflare (H/2) | Django (H/1.1) | H2.TE | TE header injected via H/2 trailer |
| Apache Traffic Server | Tomcat | TE.TE | Transfer-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.
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.
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.