Challenge Overview#
- Challenge name: Web cache poisoning via HTTP/2 request tunnelling
- Category: HTTP Request Smuggling
- Level: Expert
- Objective: Poison the cache in such a way that when the victim visits the home page, their browser executes
alert(1). A victim user will visit the home page every 15 seconds.
1. Fundamental Concepts#
1.1 HTTP/2 Downgrading & The Architectural Impedance Mismatch#
In modern enterprise architectures, Edge proxies (Load Balancers, Reverse Proxies, CDNs) negotiate HTTP/2 with client browsers over TLS. To reduce operational complexity in internal microservices, the front-end frequently translates and downgrades incoming requests to HTTP/1.1 before dispatching them downstream.
This protocol translation introduces an Architectural Impedance Mismatch:
- HTTP/2 relies on binary framing. Frames (
HEADERS,DATA) have explicit length boundaries, rendering delimiters like\r\n(CRLF) semantically inert. - HTTP/1.1 relies entirely on ASCII text parsing, where
\r\ndefines line terminations, and\r\n\r\ndefines the end of headers.
When a front-end server fails to sanitize control characters during translation, arbitrary CRLF sequences injected into binary frames are serialized directly into the plain-text HTTP/1.1 stream, allowing an attacker to manipulate downstream request framing.
1.2 The Anatomy of “Request Tunnelling” vs “Classic Request Smuggling”#
In Classic HTTP Request Smuggling (e.g., CL.TE or TE.CL):
- The front-end maintains a shared connection pool to the back-end. Multiple unrelated client requests reuse the same underlying TCP socket.
- An attacker injects a smuggled prefix that sits in the back-end socket buffer, corrupting the subsequent request of another client (Cross-user Desynchronization).
In Request Tunnelling:
- The front-end enforces a 1:1 dedicated connection lifecycle—each client connection maintains its own isolated socket to the back-end, and connections are never shared across distinct users.
- Because back-end connections are isolated, cross-user socket poisoning is impossible.
- However, if an attacker injects a secondary HTTP/1.1 request down their own dedicated channel (the “tunnel”), the back-end processes two requests in sequence on that single socket. The vulnerability then shifts to how the front-end or caching layer handles the secondary response.
1.3 Web Caching Mechanics & The Cache Key Alignment#
A Web Cache stores responses to reduce server load and latency. When a request arrives, the Cache Engine computes a Cache Key (typically composed of the HTTP Method, Host header, and Request Path, e.g., GET /).
- If a response is cacheable (e.g.,
Cache-Control: max-age=30), the Cache Engine binds the response body received from the back-end socket to that Cache Key. - In Web Cache Poisoning via Request Tunnelling, an attacker tricks the front-end into caching the response of the smuggled (tunnelled) request under the Cache Key of the outer request (e.g., the public homepage
/). Once cached, every visitor requesting the homepage receives the malicious payload.
2. Attack Model & Architecture#
2.1 The Vulnerability: CRLF Injection in the :path Pseudo-Header#
Unlike traditional request smuggling that manipulates standard headers, this attack targets the HTTP/2 :path pseudo-header.
When the front-end translates an HTTP/2 request into HTTP/1.1, it reconstructs the HTTP/1.1 Request-Line using the :path value:
| |
If the front-end parser does not enforce RFC 9113 validation on :path, an attacker can inject:
| |
When rewritten by the front-end, the downgraded stream becomes:
| |
- Notice the trailing
Foo: barheader: It captures the trailingHTTP/1.1\r\nautomatically appended by the front-end, preserving valid HTTP/1.1 syntax for the second request.
2.2 The HEAD Method Gadget#
How does an attacker force the front-end cache to store the response of the second request instead of the first?
- When an HTTP client sends a
HEADrequest, the server returns the response headers that would accompany aGETrequest, but without a response body. - By setting
:methodtoHEADon the outer request:- The back-end handles the outer
HEAD /request $\rightarrow$ returns HTTP headers, 0 body bytes. - The back-end immediately processes the queued tunnelled request (
GET /smuggled) $\rightarrow$ returns headers and response body. - The front-end expects to read a response matching the
Content-Lengthof the outer request. It reads the data off the socket, mistakenly interpreting the response of the tunnelled request as the body of the outer request! - The Cache Engine stores this tunnelled response under the Cache Key of the outer request (
GET /).
- The back-end handles the outer
2.3 The Content-Length Trap & Padding Requirement#
- A normal
GET /orHEAD /response for the homepage has a largeContent-Length(e.g., 8,634 bytes). - An unencoded redirect or error endpoint (our XSS gadget) typically returns only ~300 bytes.
- If the tunnelled response body is smaller than the expected
Content-Length, the front-end halts and waits for the remaining bytes to arrive until a socket timeout occurs (500 Server Error: Communication timed out). - The Solution: The attacker must pad the query string of the tunnelled request with thousands of dummy characters (
AAAA...) so that the reflected response body equals or exceeds the expected8,634 bytes.
2.4 Attack Workflow & Data Flow Diagram#
| |
3. Step-by-Step Exploitation (PoC)#
3.1. Phase 1: Identifying Active Caching Behavior#
- Capture a baseline request
GET /in Burp Suite and send it to Repeater. Ensure the protocol is set to HTTP/2. - Send the request and examine the response headers:
| |

- Send the request a second time within 30 seconds. The headers confirm active caching:
| |

Key Observation: The caching layer uses a 30-second TTL (
max-age=30) and exposes cache status viaX-Cache(hit/miss) andAge.
3.2. Phase 2: Probing CRLF Injection in :path#
We probe whether the front-end validates newlines within :path.
- In Burp Repeater, open the Inspector panel.
- Select the
:pathpseudo-header. - Use a cachebuster query parameter to avoid poisoning the shared cache prematurely. Enter the path and press
Shift + Enterto insert a literal newline (\n/\r\n):
| |

- Send the request. The server returns
HTTP/2 200 OKwithX-Cache: miss.- Why this works: When downgraded, the front-end serializes:
GET /?cachebuster=998877 HTTP/1.1\r\nFoo: bar HTTP/1.1\r\nThe trailingHTTP/1.1appended by the proxy becomes the value of theFooheader, preserving valid HTTP/1.1 Request-Line syntax.
- Why this works: When downgraded, the front-end serializes:
3.3. Phase 3: Verifying Request Tunnelling via HEAD Method#
Now, we leverage the HEAD method to tunnel a request for a blog post (/post?postId=1) to confirm that the secondary response can be retrieved.
- In the Inspector, change the pseudo-header
:methodtoHEAD. - Set
:pathto:
| |

- For reference, visiting
/post?postId=1directly renders the article titled “Don’t Believe Everything You Read”:

- Send the tunnelled request in Repeater. The response body now returns the HTML content of
/post?postId=1:

- Now remove
?cachebuster=2from:pathto test whether the live homepage can be poisoned:
| |

- Send the request. The public homepage
/is now poisoned: any user visiting the homepage is served the article forpostId=1!

3.4. Phase 4: Discovering the XSS Reflection Gadget#
To solve the lab, we must execute alert(1) on the victim’s browser. We need an endpoint that reflects unencoded input.
- Send
GET /resources HTTP/2. The server returns a302 Foundredirecting to/resources/:

- Test if query parameters are reflected unescaped:
| |
- The response reflects the unescaped script tag in the
Locationheader:
| |

3.5. Phase 5: Solving the Content-Length Mismatch (Padding)#
We attempt to tunnel the XSS payload into /?cachebuster=3:
- Configure
:pathin the Inspector:
| |

- Send the request. The request hangs and eventually fails with:
| |

Root Cause Analysis: The outer request (
HEAD /) expects 8,634 bytes (the size of the homepage). However, the tunnelled302 Foundresponse is only ~300 bytes. The front-end holds the socket open waiting for the missing ~8,300 bytes until the connection times out.
- Remediation via Padding: Pad the query string with approximately 8,700
As immediately following</script>:
| |

- Send the padded request. The response returns immediately with
HTTP/2 200 OK(Content-Length: 8634), with the tunnelled302 Foundcontaining the XSS payload nested inside:

3.6. Phase 6: Executing the Live Cache Poisoning Attack#
With the exploit validated in isolation, we target the live homepage:
- In the Inspector, remove
?cachebuster=3from:path, leaving only/:
| |

- Send the request repeatedly every 5 seconds to ensure that whenever the 30-second cache expires, it is immediately re-poisoned:

- Within 15 seconds, the automated victim crawler accesses the homepage
/, loads the poisoned response from the cache, executesalert(1), and completes the lab.

4. Remediation & Defense Strategies#
4.1 Strict Pseudo-Header Validation (RFC 9113 Compliance)#
The foundational flaw is accepting illegal whitespace and delimiter characters within HTTP/2 pseudo-headers.
- Validation Rules: Front-end proxies must reject any request where
:path,:method,:scheme, or:authoritycontains ASCII whitespace (0x20), carriage return (0x0D), line feed (0x0A), or NUL (0x00). - Immediate Protocol Error: Under RFC 9113 Section 8.2.1, any malformed pseudo-header field must be treated as a stream error of type
PROTOCOL_ERROR(RST_STREAM).
4.2 Adopting End-to-End HTTP/2#
- The most resilient architectural defense against request smuggling is eliminating HTTP/2 downgrading entirely.
- When both the edge proxy and internal upstream servers communicate natively over HTTP/2 end-to-end, request framing is strictly delineated by binary frame headers (
FRAME_LENGTH). CRLF injection becomes impossible because delimiters have no semantic meaning.
4.3 Secure Reverse Proxy & Cache Configuration#
Nginx#
When Nginx functions as a caching reverse proxy, ensure that URI normalization is enforced and raw unvalidated header lines are not passed downstream:
| |
HAProxy#
HAProxy provides strict HTTP/2 protocol parsing when configured in mode http:
| |
4.4 Defensive Cache Engine Architecture#
- Never Cache Bodies for
HEADRequests: A cache engine must explicitly discard any response body data if the client request method wasHEAD. - Context-Aware XSS Encoding in Redirects: Frameworks must sanitize or URL-encode any user-supplied query string before embedding it into HTTP redirect responses (
Locationheader and default HTML body).
