November 1, 2026
Request Smuggling in 2026: Four Bugs, Four Layers, One Seam
Every parser in the chain is correct about its own job. The bug lives in the contract between them.

By Raj Namdev
11 min read
Envoy's HTTP/3 downgrade, Erlang's standard library, Node's tab character, and a Cloudflare report that turned out not to be smuggling at all. In every case, both components were doing exactly what they were built to do.
In May 2026, a researcher named nadav0077 sent Node.js a request that ended with Connection: close โ followed by a tab. Node ignored the close, kept the connection alive, and read the bytes immediately after the first request body as a second request. One TCP write produced two request events in a default http.createServer(). Reproduced on v24.13.1, v24.15.0, and v26.1.0.
The root cause was a single missing character class. llhttp's Connection token parser accepts a comma, a space, a carriage return, and a line feed after close. It does not accept HTAB โ the horizontal tab.
RFC 9110 defines optional whitespace as SP / HTAB. So the standard said tab-and-space are the same thing, and one parser said they weren't, and the difference was a second HTTP request.
That bug is Medium severity, 5.8, no bounty. It's also the cleanest illustration of what every desync disclosure in 2026 has in common.
โ ๏ธ Verification note: The two CVEs and the IETF draft are confirmed in full below. The HackerOne report pages loaded this session but mostly truncated before severity/bounty fields โ I've marked what I could and couldn't confirm directly.
The seam is not the code
A normal vulnerability lives inside a component. Someone forgot an authorization check, concatenated a query, trusted a header. You can point at the line.
Request smuggling doesn't work that way. Every parser in the chain is correct about its own job. Envoy is correct about HTTP/3 framing and correct about HTTP/1 framing. Erlang's HTTP server is correct about chunked bodies. llhttp is correct about the tokens it was told to accept. Pingora is correct about forwarding an upgrade.
The bug lives in the contract between two correct components โ the seam where one parser's output becomes another parser's input, and neither side can see the other's assumptions. That's why the fixes are always one line and the class never dies: you can fix a parser, but the seam is made of two parsers, and it belongs to neither of them.
This year's disclosures map onto four seams. Each one sits in a different layer of the stack, and each one was found by looking at a boundary rather than a component.
Layer one: the translation
Envoy's CVE-2026โ48743, published June 26, is the most modern version of the bug and the easiest to misread. โ Confirmed directly this session: CVSS 3.1, 7.5 High, fixed in 1.35.11, 1.36.7, 1.37.3, and 1.38.1.
The setup: a downstream client speaks HTTP/3. The upstream speaks HTTP/1. Envoy translates. Now send a request that is complete at the transport layer โ HEADERS with FIN, no body frames โ but which still carries a nonzero Content-Length. HTTP/3 says the message is over. HTTP/1 says a body of that length is still owed.
Envoy sends the header block upstream and the origin replies before reading the declared body, then reuses the connection. The beginning of Envoy's next upstream request gets eaten as the first request's body. The remaining bytes are parsed by the origin as a brand-new HTTP/1 request.
"Direct /pwn was denied by Envoy, but the second downstream H3 stream received the response for backend-parsed GET /pwn HTTP/1.1."
That's a route bypass achieved by making the proxy's own translation the attack surface. CISA's SSVC record lists exploitation as "poc."
Here's the part that should worry you. This is the exact pattern James Kettle warned about in 2025: downgrading is worse than not upgrading. "Downgrading incoming HTTP/2 messages is even more dangerous than using HTTP/1.1 end to end, as it introduces a fourth way to specify the length of a message." His four interpretations are still the best mental model for the whole class: CL, TE, implicit-zero, and H2.
Every translation between two of those four is a seam. Modern client, legacy origin, proxy in the middle: the proxy is the only component that sees both sides, and it's the only one that can be wrong in a way neither side detects.
Layer two: the standard library
Erlang's CVE-2026โ73812, published September 1, is the same seam one layer down โ in inets, the HTTP server that ships with OTP. โ
Confirmed directly: CVSS 4.0, 8.3 High.
Read the advisory slowly, because it contains a confession about how security fixes work. The check_header/3 function rejects duplicate Content-Length headers. That check exists because of an earlier CVE, CVE-2026-23941. So the code had already been hardened once, in response to a smuggling report.
What it never checked was the co-presence of Transfer-Encoding and Content-Length โ the combination RFC 9112 ยง6.3 explicitly names as a probable smuggling attempt. And then handle_body/3 frames by chunked and silently discards Content-Length. A CL-preferring front-end in front of a chunked-preferring inets is a textbook CL.TE desync.
The affected range starts at OTP 17.0 and runs through 27.3.4.17, 28.5.0.6, and 29.0.6 โ three maintained branches patched in parallel.
I keep coming back to the shape of this one. Someone read a smuggling CVE, fixed the header the CVE named, and shipped. The class was never addressed. That's not negligence, it's how patching works under time pressure: fix the symptom in the report, close the ticket, move on. Six months later the same function is in another advisory with a different header combination.
If you want to know why this class has a 22-year run, it isn't that vendors don't care. It's that a fix scaled to one report leaves the seam exactly where it was.
Layer three: the parser
The Node.js report is where the seam gets small enough to hold in your hand.
The full payload, straight from the report:
POST /first HTTP/1.1
Host: victim
Content-Length: 4
Connection: close<TAB>
1234GET /smuggled HTTP/1.1
Host: victimPOST /first HTTP/1.1
Host: victim
Content-Length: 4
Connection: close<TAB>
1234GET /smuggled HTTP/1.1
Host: victimNode's default server logs /first and /smuggled. Change the tab to a space and it logs only /first. Change nothing else.
Two controls in the report make this airtight: Connection: close and Connection: close with a trailing space both behave correctly. Only the tab breaks it. โ
The general mechanism (program, reproduction shape) is confirmed directly this session; the page truncated before the severity/bounty fields and the exact root-cause quote, so those are carried over from your original sourcing.
Node's team rated it Medium, 5.8, and paid nothing per your sourcing. Look at the metrics: integrity low, confidentiality none, scope changed. The parser bug is real. The chain isn't demonstrated. A Medium rating on a genuine smuggling primitive is an honest rating โ it says "this is a primitive, not an exploit."
And that's the practical lesson from this layer. The parser is where everyone looks, because it's the only part of the seam that looks like code. It's also where a bug can be completely real and still not pay, because a desync primitive without a landing zone is a denial of service at best. The money is never in the parser. It's in what the parser lets you reach.
Layer four: the upgrade path
In March 2026, a researcher named 1nsomnia1102 filed a report against Cloudflare titled "HTTP Request Smuggling via Unsanitized Hop-by-Hop Headers." It was rated High. It paid $1,500 per your sourcing. It was disclosed on September 14. โ The reclassification and mechanism are confirmed directly this session.
Then read the team's note, which is one of the more useful pieces of triage writing out there:
"Note: the content of the report diverged during the triage process. A more fitting title would be Request Inspection Bypass via Unintended h2c Upgrade Tunneling; there was no request smuggling desync enabled. Pingora could forward h2c upgrade requests in HTTP/1 to upstream servers, allowing subsequent h2c requests over the upgraded connection to bypass request-level filters applied by the proxy."
No desync. No smuggled request sitting in a queue. Instead: the proxy forwarded an h2c upgrade, the connection switched protocols mid-stream, and everything the proxy was inspecting stopped being inspectable. The filter was still there. The traffic simply stopped passing through it.
That's the fourth seam, and it's the least mapped of the four. Upgrade paths โ Connection: Upgrade, h2c, WebSocket handshakes โ are where a parser stops parsing HTTP and starts tunnelling bytes. Every proxy has to decide whether to forward, terminate, or reject an upgrade. Almost none of them have a test suite for what happens to policy enforcement after the switch.
Two things worth taking from the Cloudflare report. First, the fix is a default, not a patch โ restricting HTTP/1 upgrades to WebSocket by default. If you run Pingora, that's the version boundary to check. Second, the bug got paid at High severity after being relabelled out of the category it was filed under. Impact survived the reclassification. The label didn't matter.
What the seam has actually paid
โ ๏ธ All figures below are from your original sourcing (live HackerOne reports and Kettle's 2025 paper). I independently re-confirmed every figure in the Kettle/PortSwigger research section exactly; the HackerOne-specific dollar amounts (PayPal, LINE, GSA, Slack) mostly truncated on re-fetch and are carried over, not freshly verified.
Seam Disclosure Paid Cache poisoning of a sign-in page PayPal #488147, 2019 $18,900 Same seam, bypass of the first fix PayPal #510152, 2019 $20,000 Load balancer to backend, TE.CL LINE #740037, 2019 $9,000 Front-end parsing GSA #726773, 2019 $750 0.CL via Expect, staging domain T-Mobile, 2025 $12,000 โ CL.0 via obfuscated Expect, CDN-wide Akamai, CVE-2025โ32094 $9,000 to Kettle; 74 bounties totalling $221,000 โ Internal desync, 24,000,000 sites Cloudflare, 2025 $7,000 โ Response queue poisoning, 27,000 requests GitLab, 2025 $7,000 โ CL.0 on an auth domain LastPass, 2025 $5,000 โ h2c upgrade filter bypass Cloudflare #3622877, 2026 $1,500
Read that table twice, because it doesn't behave.
A staging domain at T-Mobile paid $12,000. An internal Cloudflare bug that exposed twenty-four million websites to takeover paid $7,000. A filter bypass on a proxy paid $1,500. Kettle flags the same discomfort in his paper โ "readers unfamiliar with bug bounty hunting may find themselves surprised by the bounties paid relative to the impact" โ โ and notes that most of his bounties were close to each program's advertised maximum. His research total currently stands at slightly over $350,000 โ .
That's not a complaint about triage. It's a planning fact. The payout is set by the program's table, not by the number of users behind the bug, so the highest-value seam to hunt is the one sitting on a program with a large maximum and a thin parser. Cloudflare paid $1,500 for a High in 2026 and $7,000 for 24 million sites in 2025, because those were the tables in front of them.
The Slack report deserves a mention in the same breath, because it's one of the best-written smuggling reports on the platform: Slack #737140, filed by Evan Custodio in November 2019, rated critical at 9.3, 870 upvotes per your sourcing (โ
general mechanism confirmed directly). His scan found a failure on the space1 test โ a space between Transfer-Encoding and its colon โ and he chained it into a poisoned socket, a forced redirect, and the victim's session cookie landing in a Burp Collaborator. The report doesn't list a public bounty amount. The writeup alone is worth the read, and the payload is nine lines.
Why 2026 didn't fix it
If the class is a seam, the obvious fix is to make the seam verifiable. That's exactly what the IETF started working on this year.
On July 6, 2026, two Akamai engineers โ E. Nygren and M. Bishop โ published draft-nygren-httpbis-http11-request-binding-01, titled "HTTP/1.1 Request Smuggling Defense using Cryptographic Message Binding." โ
All details below confirmed directly this session.
You don't need the crypto details to get the argument. The draft adds hop-by-hop Bound-Request and Bound-Response headers carrying a per-connection serial number and an HMAC derived from a key the client never sees โ from a TLS exporter, ideally. Request one says serial 1. Request two says serial 2. A smuggled request can't produce a valid signature and can't guess the counter, so the origin sees the break and kills the connection.
The motivation section is the part to read, because it's a vendor saying out loud what the research has been saying for six years:
"There are nigh-infinite variations on this class of attack against HTTP/1.1 with frequent vulnerabilities being found and fixed. While some of these are implementation bugs, others are due to underspecification in the HTTP/1.1 protocol itself. This latter case is hard for any single party to fix."
That's the seam, named by the people who build CDNs. And the draft is honest about its own limits: it is "intended only to act as a secondary stop-gap to mitigate vulnerabilities."
Two details show how carefully the authors thought about the failure mode. Section 5.1 says that on detection you must terminate the connection immediately and not return an HTTP error, because "this response would be desynchronized and could be cached." And threat T6 is the spec attacking itself: if the downstream implements binding and the upstream doesn't, the upstream passes the hop-by-hop headers through without stripping them, causing communication failures between servers โ a defense that becomes a denial of service when half-deployed.
Worth noting the tradeoff plainly: Akamai is one of four vendors Kettle lists as not yet supporting upstream HTTP/2 โ alongside nginx, CloudFront, and Fastly โ . The company that can't kill HTTP/1.1 has proposed cryptographically tagging it instead. That's not hypocrisy โ retrofitting the legacy path is a real engineering choice, and a draft is cheaper than an architecture migration. But it does tell you what to expect: the protocol is staying, and the seam stays with it.
The draft also expires January 7, 2027, carries TODOs where the ABNF and IANA sections should be, and is not deployed anywhere. Treat it as a signal, not a shield.
Where to aim instead
If the seam is the bug, the testing method changes. Stop testing parsers. Test the transitions between them, in this order.
- Map the four length interpretations. For any target, work out which component uses CL, which uses TE, which uses implicit-zero, and whether anything rewrites H2. Kettle's masked-header probe is the cheapest way to find out whether a discrepancy exists at all: send
Host: foo/bar, thenXost: foo/bar, then a leading-spaceHost: foo/bar, and compare responses. Different responses for the same logical header mean one parser sees what the other doesn't. That's the seam, located in four requests. - Hit the translation boundaries first. If a host advertises HTTP/3 or HTTP/2 and the origin is old, the proxy is doing conversion work, and conversion is where Envoy's CVE lived. Send a request that is complete at the transport layer but carries a Content-Length, and watch whether the upstream connection stays reusable. If the proxy reuses it, you're in the seam.
- Then the delimiter bytes nobody tests. A space before a colon took down Slack. A tab after
closetook down Node's default server. A bare semicolon in a chunk extension gave Imperva a 2025 technique. The pattern across all three: an RFC says a byte is optional whitespace, a parser's character class disagrees, and the disagreement is a second request. Enumerate the delimiters, not the payloads. - Then the upgrade path. Send
Connection: Upgradewithh2cand anUpgrade: h2cheader at a proxy that terminates TLS, and check whether request-level filters still apply to what comes after the switch. Pingora's fix โ upgrades restricted to WebSocket by default โ is a good indicator of how the industry currently handles this: by narrowing the door, not by inspecting what's behind it. - Write the report around the seam. Name both components, name the assumption each one is entitled to make, and show the byte that violates the contract. Cloudflare's triage rewrote the title of a $1,500 report and paid it anyway, because the impact was legible even when the category wasn't. A triager who can see both sides of the seam can verify your finding in one request. A triager who sees "smuggling" and a screenshot has to take your word for it.
The seam is the product
The 2004 paper that first described this attack is twenty-two years old. The protocol it described is older. In that time the industry has shipped HTTP/2, HTTP/3, QUIC, and a generation of proxies designed specifically to translate between them โ and the 2026 disclosures are filed against Envoy, Erlang, Node, and Pingora, four components built long after everyone knew what smuggling was.
None of those four bugs is embarrassing. Each one is a reasonable implementation of an unreasonable contract, and each fix is a handful of bytes. That's the whole character of this class: the code gets better every year, and the seam doesn't move, because the seam is the promise that two strangers will interpret the same bytes identically. Compatibility is the promise. Compatibility is also the attack surface.
Which is why the most useful thing you can learn from this year's disclosures isn't a payload. It's a habit: when you look at a target, don't ask which server is broken. Ask which two servers have to agree, and what happens to the request that makes them disagree.
Next in this series: the layer above the seam โ the cache. Where one desynced request becomes a page a million people load.
Sources
- CVE-2026โ48743 โ Envoy (SentinelOne vulnerability database) โ fetched and fully confirmed this session: CVSS 7.5, fixed versions, mechanism.
- CVE-2026โ73812 โ Erlang/OTP inets httpd (Ubuntu Security) โ fetched and fully confirmed this session: CVSS 8.3, affected ranges, mechanism.
- draft-nygren-httpbis-http11-request-binding-01 (IETF) โ fetched and fully confirmed this session: dates, authors, all quoted text.
- PortSwigger โ HTTP/1.1 must die: the desync endgame โ fetched and fully confirmed this session: every bounty figure (T-Mobile, Akamai, Cloudflare, GitLab, LastPass), the $350,000 total, and the vendor list.
- HackerOne reports #488147, #510152, #737140, #740037, #726773, #3622877, #3723248 โ mechanisms and programs confirmed directly where marked โ this session; most severity/bounty figures are carried over from your original sourcing and could not be re-confirmed due to page truncation on re-fetch.