Part 6 — Advanced web and API exploitation
G1 Part 10 covered the OWASP classes: injection, XSS, CSRF, SSRF, broken access control, auth. This Part is the graduate seminar: the bug classes that need protocol-level understanding, that chain into RCE or account takeover, and that the first guide deliberately left out. Every chapter is exploit-then-fix, and the practice targets are PortSwigger Web Security Academy (which has a dedicated, free, labbed track for almost every topic here) plus apps you deploy so you can also remediate.
Chapter 30 — HTTP request smuggling and desync
In one sentence
When a front-end (CDN, load balancer, reverse proxy) and a back-end disagree about where one HTTP request ends, an attacker can prepend bytes to the next person's request — bypassing front-end security, stealing other users' requests, and poisoning responses.
Where we are
Modern web serving is layered: browser -> CDN -> load balancer -> app server, often with HTTP/2 in front and HTTP/1.1 behind. Every hop re-parses the request. Smuggling exploits parsing disagreements.
How it works
The classic ambiguity: Content-Length vs Transfer-Encoding. A request can
declare its body length two ways. If a front-end honours one and the back-end the
other, bytes "leak" past the boundary.
CL.TE -- front-end uses Content-Length, back-end uses Transfer-Encoding:
POST / HTTP/1.1
Host: victim
Content-Length: 6
Transfer-Encoding: chunked
0
G
Front-end: body is 6 bytes ("0\r\n\r\nG"), forwards the whole thing.
Back-end: chunked; "0\r\n\r\n" ends the request; "G" is left in the
buffer and PREPENDED to the next request on that connection ->
"GPOST / HTTP/1.1 ..." (a broken method for the victim), or a fuller
payload smuggles a complete request.
TE.CL -- front-end uses TE, back-end uses CL: mirror image, chunk-size
line games.
TE.TE -- both do TE, but one is tricked into NOT processing it via
obfuscation:
Transfer-Encoding : chunked (space before colon)
Transfer-Encoding: xchunked
Transfer-Encoding:\tchunked
Transfer-Encoding\n : chunked
(two TE headers, one valid one junk)
HTTP/2 makes it worse via downgrade. HTTP/2 has an unambiguous, explicit
length — so h2-to-h2 is safe. But a front-end that speaks h2 to the client and
downgrades to h1 for the back-end must synthesise Content-Length/TE, and:
H2.CL / H2.TE the h2 request declares one length; the front-end
rewrites to h1 with a conflicting CL or an injected TE that the
back-end honours.
H2 CRLF INJECTION h2 header names/values can contain bytes that,
when serialised to h1, become "\r\n" -> the attacker injects entire
headers or a request line -> request splitting.
H2 request smuggling via header-name/pseudo-header abuse.
Client-side desync (James Kettle, 2022): the browser itself is the
smuggling engine. A malicious page makes a fetch() whose body is left
straddling the connection boundary; the next request the browser sends on that
reused connection is poisoned — no back-end proxy required, and it works against
the victim's own authenticated session.
Impact:
- bypass front-end controls: smuggle a request to /admin past a WAF /
auth proxy that only inspected the outer request.
- capture other users' requests: smuggle a request whose body is a
large parameter; the victim's following request gets appended into
that parameter and (if the app stores/reflects it) you read their
headers, cookies, CSRF tokens.
- response queue poisoning: desync the response stream so User B gets
User A's response (or your injected one) -> mass credential/session
theft, cache poisoning (Ch 31).
- forced browser cache poisoning / persistent XSS via the above.
Detection: timing probes (a partial smuggled request makes the next request
hang for the socket timeout — a reliable differential), and differential
responses. Tools: Burp HTTP Request Smuggler extension, smuggler.py,
h2csmuggler, Burp's "Turbo Intruder" for the concurrency.
Worked example
Bypassing a front-end auth check.
Front-end proxy blocks /admin unless the request has a valid staff
session; forwards everything else. Back-end trusts the front-end.
CL.TE smuggle:
POST /feedback HTTP/1.1
Host: shop.securesh.op
Content-Length: 116
Transfer-Encoding: chunked
0
GET /admin/delete?user=victim HTTP/1.1
Host: shop.securesh.op
X-Ignore: X
Front-end: sees /feedback (allowed), CL=116, forwards all bytes.
Back-end: chunked -> request ends at "0\r\n\r\n"; the rest
("GET /admin/delete...") starts the NEXT request -- which the
back-end processes WITHOUT the front-end ever seeing "/admin".
The next legitimate user's request completes the smuggled one (or
provides its Host/cookies). Admin action executed.
FIX:
- Use HTTP/2 end to end; do NOT downgrade to h1 at the back hop.
- If you must downgrade: the front-end must reject any request with
BOTH Content-Length and Transfer-Encoding, or with a malformed/
obfuscated TE, or with header-name/value bytes that don't
round-trip -- normalise or 400.
- Back-end: don't trust the front-end for auth; re-check on the
back-end (assume-breach, Ch 3).
- Disable back-end connection reuse across different client
connections, or bind a back-end connection to one client -> a
smuggled prefix can't reach another user.
- Consistent, strict HTTP parsers on every hop (same library/config).
Practice (90 min)
- PortSwigger Academy "HTTP request smuggling" track — do CL.TE, TE.CL, TE.TE, obfuscation, then the H2 downgrade and CRLF-injection labs, then the response-queue-poisoning and request-capture labs, then client-side desync.
- Install Burp's HTTP Request Smuggler; run its scan against a lab; read how the timing probe works.
- Stand up a deliberately-mismatched stack locally: an old
nginx/haproxyin front of a permissive back-end (or use thehttp-garden/smugglerresearch harness). Reproduce CL.TE. Then fix by upgrading/normalising and re-test. - Configure an h2 front-end that downgrades to h1; try H2.CL. Then set it to h2 end-to-end and confirm the attack dies.
Across the series
- Reproduce the root cause safely: the HTTPS guide's
desynclab sends one message with bothContent-LengthandTransfer-Encodingto a Go backend, which handles it as two requests. It also shows why a Go handler can't detect the conflict, and why re-serialising proxies help.
Common mistakes
- Front-end downgrading to HTTP/1.1 for the back-end. The single biggest enabler of modern smuggling. Go h2 end-to-end.
- Trusting the front-end for security decisions. Re-authenticate and re-authorise on the back-end.
- Accepting requests with both CL and TE, or with obfuscated TE. Reject (400), don't "pick one".
- Reusing back-end connections across client connections. Lets a smuggled prefix land on someone else's request. Bind or disable.
- Different HTTP parsers/configs on each hop. Normalise; use the same strict library everywhere.
- Assuming a WAF stops it. The WAF is a hop too, and often the one being bypassed.
Check yourself
- What disagreement makes CL.TE smuggling work, and what ends up in the back-end's buffer?
- Why is HTTP/2 end-to-end safe, and how does an h2->h1 downgrade reintroduce smuggling?
- What is client-side desync and why doesn't it need a back-end proxy?
- Name three impacts of a successful smuggle beyond "bypass a filter".
- Give four defences (front-end normalisation, transport, connection handling, back-end trust).
(Answers: Appendix G.)
Further reading
- Research: PortSwigger — "HTTP Desync Attacks: Request Smuggling Reborn" (2019), "HTTP/2: The Sequel is Always Worse" (2021), "Browser-Powered Desync Attacks" (2022) — James Kettle. The definitive material.
- Labs: PortSwigger Academy request-smuggling track (free).
- Tools: Burp HTTP Request Smuggler,
smuggler.py(defparam),h2csmuggler(BishopFox),http-garden(differential HTTP parser fuzzer). - RFC: RFC 9112 (HTTP/1.1 message syntax) sections on message length; RFC 9113 (HTTP/2).