HTTP Request Smuggling exploits a subtle disagreement between two machines that process the same request: the front-end (a reverse proxy, CDN, or load balancer) and the back-end (the application server behind it). When the two disagree about where one request ends and the next begins, the attacker can append — smuggle — a prefix of a second request that the front-end treats as part of the first request’s body, but that the back-end interprets as the start of an independent request.

This smuggled prefix sits “waiting” on the reused TCP connection between front-end and back-end, and attaches itself to the next request that travels over that connection — often someone else’s. The result is one of the most impactful classes of flaw in the modern web: bypassing front-end security controls, poisoning caches, capturing victims’ requests and cookies, and hijacking sessions — all without ever touching their browsers directly.

The anatomy of the attack

HTTP/1.1 offers two ways to indicate the length of a request body, and that redundancy is the root of the problem:

  • Content-Length (CL): states how many bytes the body has. E.g., Content-Length: 13.
  • Transfer-Encoding: chunked (TE): the body comes in blocks (chunks), each prefixed by its size in hexadecimal, ending with a block of size 0 followed by a blank line (0\r\n\r\n).

The specification is clear: if both appear in the same request, Transfer-Encoding takes precedence and Content-Length must be ignored — and the server may (under RFC 9112, which obsoleted RFC 7230 in 2022, should) treat the message as invalid and respond with 400, closing the connection. The problem is that not every server follows the rule. When the front-end uses one header and the back-end uses the other, they desync about the message boundary.

CL.TE — front-end uses Content-Length, back-end uses Transfer-Encoding

Here the front-end honors Content-Length and forwards the entire message; the back-end honors Transfer-Encoding: chunked and stops reading at the 0 chunk, leaving the rest “orphaned” on the connection:

POST / HTTP/1.1
Host: app.exemplo.com
Content-Length: 6
Transfer-Encoding: chunked

0

G

The front-end reads the 6 bytes announced in Content-Length (0, CRLF, CRLF, G = 6 bytes) and passes everything along. The back-end reads the 0 chunk and considers the request finished there0\r\n\r\n. The remaining byte (G) stays in the connection’s buffer and will serve as the prefix of the next request that arrives from the front-end. When another user makes a POST / HTTP/1.1, it will be concatenated with our orphaned prefix, becoming GPOST / HTTP/1.1 — a Frankenstein request. In a real attack, this prefix is larger: you smuggle the start of an entire request (a route, headers), not just a single byte.

TE.CL — front-end uses Transfer-Encoding, back-end uses Content-Length

The mirror image of the previous case. The front-end processes the chunks; the back-end looks only at Content-Length and reads just the first bytes, leaving the rest of the chunk as the smuggled prefix:

POST / HTTP/1.1
Host: app.exemplo.com
Content-Length: 3
Transfer-Encoding: chunked

8
SMUGGLED
0

The front-end (chunked) reads the chunk of size 8 (SMUGGLED) and the 0 terminator, consuming the whole body. The back-end (CL) reads only 3 bytes (8\r\n) and considers the request finished — the SMUGGLED\r\n0\r\n\r\n becomes the prefix of the following request.

Teaching note: in the examples above, the value declared in Content-Length must match the body’s byte count exactly — including the \r\n (CRLF). In the CL.TE example, 0\r\n\r\nG is 6 bytes; in TE.CL, 8\r\n is 3 bytes. That’s why fine-tuning the CL is the trickiest part of reproducing a smuggling attack in practice.

The key point, in any variant: a single TCP connection between front-end and back-end is reused to serve multiple users, and the orphaned prefix we leave behind poisons the request of whoever comes next.

Variations and bypasses

TE.TE — both support chunked, but one is fooled by obfuscation

When both servers understand Transfer-Encoding, you can still desync them by making one of them fail to recognize the header. The technique is to obfuscate the Transfer-Encoding in a way that one server accepts and the other discards. Some obfuscation examples (the \r/\t represent real bytes — CR and TAB):

Transfer-Encoding: chunked
Transfer-Encoding: identity

A second header with a different value makes some parsers honor the last one (identity, no chunked) and others the first. Other common variants, byte by byte:

Transfer-Encoding:[tab]chunked      # tab instead of a space after the colon
Transfer-Encoding[space]: chunked   # space before the colon
Transfer-Encoding: chunked[CR]      # lone CR before the end-of-line CRLF

If the front-end ignores the obfuscated header (falling back to Content-Length) and the back-end accepts it — or vice versa — we have an exploitable TE.TE. The catalog of obfuscations is large because each HTTP parser has its own quirks; the HTTP Request Smuggler extension already ships dozens of them.

CL.0 — when Content-Length is treated as zero

On endpoints that don’t expect a body (e.g., a GET or a static resource served directly by the back-end), some back-ends ignore Content-Length and treat the body as nonexistent, while the front-end honors it and forwards it. The entire body “leaks” into the next request:

POST /recurso-estatico HTTP/1.1
Host: app.exemplo.com
Content-Length: 44

GET /admin HTTP/1.1
Host: app.exemplo.com

The declared body (GET /admin HTTP/1.1\r\nHost: app.exemplo.com\r\n) is exactly 44 bytes. The back-end serves /recurso-estatico ignoring the body, and the GET /admin... becomes the prefix of the next request on the connection.

Desync via HTTP/2 downgrade (H2.CL / H2.TE)

This is the most relevant vector today. HTTP/2 doesn’t use textual headers to delimit messages — the body length is embedded in the frames (DATA frames with explicit size), which makes classic smuggling impossible within HTTP/2 itself. The danger appears when the front-end speaks HTTP/2 with the browser but downgrades to HTTP/1.1 when talking to the back-end. At that moment, it reconstructs the HTTP/1.1 message — including Content-Length/Transfer-Encoding — from what it received over HTTP/2, and if it doesn’t sanitize, it reintroduces the ambiguity:

  • H2.CL: we inject a lying content-length as an HTTP/2 header; if the front-end copies it into the translated HTTP/1.1 request instead of recalculating the real size, the back-end desyncs.
  • H2.TE: we inject transfer-encoding: chunked via HTTP/2; if the front-end doesn’t reject it (RFC 9113 forbids Transfer-Encoding in HTTP/2, but not every front-end validates), it appears in the translated HTTP/1.1 message and the back-end starts using chunked.

Related downgrade vectors include CRLF injection inside HTTP/2 header/pseudo-header values (which become real line breaks in the translation to HTTP/1.1) and request splitting, enabling desync even against back-ends that seemed immune. The lesson: the downgrade is where HTTP/2’s guarantees die.

How we exploit it during a pentest

Scope warning. Detecting request smuggling is intrusive by nature: a smuggled prefix can attach itself to a real user’s request and break their session, corrupt responses, or pollute the production cache. Only run it with explicit written authorization, preferably in a staging environment, outside peak hours, and never leave “armed” prefixes dangling on the connection.

The workflow we use in the field:

1. Timing-based detection (induced delay). The safest technique to confirm desync without affecting third parties. We send a malformed request that, if there is a desync, makes the server wait for bytes that never arrive — producing a measurable delay. Example of a CL.TE probe: we declare a Content-Length larger than the body sent; the front-end, reading chunked, ends the request at the 0 chunk, but the back-end, reading CL, stays blocked waiting for the missing bytes until timeout. Without desync, the response returns normally; with desync, it hangs. Time difference = signal. (The exact direction of the probe depends on which side reads CL and which reads TE; HTTP Request Smuggler tests both.)

2. Confirmation via differential response. We send two requests in sequence: the first smuggles a prefix that should alter the second request’s response (e.g., force a 404 or a redirect). If the second request comes back different from expected, we confirm the prefix was injected. Done well, this approach affects only our own subsequent requests (sent over the same connection, in quick succession), which makes it more controlled than waiting for a victim — though there’s still a residual risk that the window “leaks” to another user.

3. Tools. In practice, we automate with:

  • HTTP Request Smuggler (a Burp Suite extension, by James Kettle/PortSwigger): performs the timing scan, tests the CL.TE / TE.CL / TE.TE matrix and the obfuscations automatically, and helps nail down the exact Content-Length.
  • Turbo Intruder: to fire the requests with the fine-grained connection and concurrency control that smuggling requires (keeping the connection open, synchronizing the send, batching requests into the same packet).
  • Param Miner: useful for discovering headers and cache behaviors that amplify the web cache poisoning derived from the desync.

4. Escalation. Once the desync is confirmed, we measure real impact: smuggling a request to an administrative route bypassing the front-end’s access control (the front-end never “sees” that route, so it never applies its rules); capturing another user’s request by making the back-end reflect the victim’s body back to us (leaking cookies and tokens in a controlled field, such as a search parameter or a stored comment); or poisoning the cache to serve malicious content to everyone who requests that resource.

Summary for the report

  • Impact: bypass of front-end security controls (access to restricted routes), capture of other users’ requests/cookies, session hijacking, and cache poisoning affecting the entire user base.
  • Severity: High to Critical. Example CVSS v3.1 vector AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:L = 8.9 (scope changed because it affects third-party requests; AC:H reflects the timing/Content-Length fine-tuning required). The exact score varies depending on the proven impact.
  • Preconditions: an architecture with a front-end and back-end that reuses TCP connections and disagrees about Content-Length vs Transfer-Encoding (or downgrades from HTTP/2 to HTTP/1.1 without sanitizing).
  • Suggested evidence: the raw smuggled request (with CL/TE highlighted), the delay measured in the timing proof, the request→response differential pair showing the injection, and — where applicable — a captured victim/poisoned-cache response (with sensitive data redacted).

How to mitigate

The fix isn’t an application patch — it’s architectural. The goal is to eliminate the ambiguity at the message boundary and the poisonable connection reuse.

1. End-to-end HTTP/2 (the defense that fixes the root cause)

If the front-end speaks HTTP/2 with the client and HTTP/2 with the back-end, there’s no CL/TE header reconstruction and the length comes in the frames — both classic and downgrade smuggling disappear. Avoid downgrading to HTTP/1.1 on the internal hop whenever possible. When the downgrade is unavoidable, the front-end must rigorously sanitize the translated headers: recalculate Content-Length from the real body, reject values containing CR/LF, and discard any transfer-encoding/content-length injected by the client via HTTP/2.

2. Reject ambiguous messages at the front-end

The front-end is the front line: it should normalize or refuse any request that presents Content-Length and Transfer-Encoding together, or an obfuscated Transfer-Encoding. Illustrative configuration (NGINX as a hardening example):

# Wrong: forward the request as-is, with conflicting CL and TE
# location / { proxy_pass http://backend; }

# Correct: reject ambiguous requests before passing them to the back-end
server {
    # Modern NGINX versions already normalize most of these cases;
    # enforce the policy explicitly:
    underscores_in_headers off;        # don't accept ambiguous headers with '_'

    location / {
        # Strict policy: this path does not accept a chunked body.
        # Blocks any request that carries Transfer-Encoding.
        if ($http_transfer_encoding) {
            return 400;
        }
        proxy_http_version 1.1;
        proxy_pass http://backend;
    }
}

The example is didactic and strict (refusing all Transfer-Encoding breaks legitimate clients that rely on chunked, so validate the rule for your traffic). The exact policy depends on the product (NGINX, HAProxy, Envoy, CDN). The principle is universal — front-end and back-end need to agree on how to delimit the body, and the front-end must refuse anything ambiguous instead of “trying to guess.”

3. Ensure parsing consistency across layers

Ideally, front-end and back-end use the same server implementation (or versions with identical, strict parsing behavior). A back-end that follows the RFC to the letter — Transfer-Encoding takes precedence, and a request with CL+TE is rejected with 400 and the connection closed — shuts down the CL.TE/TE.CL variants. At the application level, prefer frameworks/servers that reject ambiguous bodies over tolerant ones.

# Conceptual example of strict validation at the back-end (defensive).
# Assumption: 'headers' already normalizes names to lowercase and detects duplicates.
def validar_delimitadores(headers):
    tem_cl = "content-length" in headers
    tem_te = "transfer-encoding" in headers
    if tem_cl and tem_te:
        raise BadRequest(400)          # ambiguous: reject, don't guess
    if tem_te and headers["transfer-encoding"].strip().lower() != "chunked":
        raise BadRequest(400)          # TE with an obfuscated/unknown value
    # Note: DUPLICATE headers (two Content-Length or two
    # Transfer-Encoding) must also be rejected before this point.

4. Don’t reuse poisonable TCP connections to the back-end

If the front-end does not reuse the connection to the back-end across requests from different users (or opens one connection per request), the orphaned prefix has nowhere to “wait” for the victim. It’s a real performance cost, but it’s a powerful containment layer in architectures that can’t migrate to end-to-end HTTP/2.

5. Keep everything up to date

Most obfuscation variants have been fixed in recent versions of proxies, CDNs, and application servers. Update front-ends (NGINX, HAProxy, Envoy, CDN) and back-ends regularly — much of the smuggling exploited in the field lives off outdated components.

Mitigation checklist

  • End-to-end HTTP/2; avoid downgrading to HTTP/1.1 on the internal hop.
  • If there is a downgrade, sanitize the translated headers (recalculate CL, reject CR/LF and TE/CL injected via HTTP/2).
  • Front-end rejects requests with simultaneous Content-Length and Transfer-Encoding.
  • Front-end rejects obfuscated Transfer-Encoding (spaces/tabs, unexpected value, duplicate header).
  • Back-end follows the RFC: TE takes precedence and an ambiguous request returns 400 and closes the connection.
  • Consistent parsing between front-end and back-end (same implementation/version when possible).
  • Don’t reuse poisonable TCP connections to the back-end (or isolate per user).
  • Proxies, CDNs, and application servers up to date.
  • Monitor anomalous responses and session breaks that indicate desync in production.

Request smuggling is what happens when two machines read the same sentence and stop at different points. All the attack’s sophistication boils down to exploiting that disagreement over punctuation between the front-end and the back-end — and all mature defense boils down to ensuring that both read the request in exactly the same way, or refuse anything ambiguous.