
Lab Writeup: PortSwigger – 0.CL Request Smuggling
September 9, 2025
First-person, A complete, beginner-friendly walkthrough of PortSwigger’s new 0.CL Request Smuggling lab. Includes detection techniques, exploitation methods, ERG choices, and defensive strategies.
Lab Writeup: PortSwigger - 0.CL Request Smuggling
⚠️ Disclaimer
This write-up is for educational purposes only and intended for legal, controlled environments such as PortSwigger’s Web Security Academy. Do not apply these techniques to systems without proper authorization.
1 - Introduction
When I opened the PortSwigger 0.CL Request Smuggling lab, the lab description was clear:
Goal: Carlos visits the homepage every five seconds. Exploit the vulnerability to execute
alert()
in his browser.
So my job was simple to state and tricky to execute: make Carlos’s browser run a JavaScript alert()
by abusing a 0.CL request smuggling desync.
In this blog I write what I actually did - step-by-step, in plain language. I explain why each step matters, why I chose certain endpoints, and how defenders can prevent it.
2 - Quick background
Request smuggling happens when two servers disagree about where one HTTP request ends and the next begins. In this lab, the mismatch is 0.CL: the front-end ignores Content-Length: 0, but the back-end respects it and reads a body that the front-end didn’t expect. That mismatch lets us sneak a new request past the front-end. It doesn’t see it, but the back-end does - and runs it like a normal request.
Beginner breakout: Imagine two people reading a letter. The first one thinks the letter ends after the first page because it says 'End of letter' (Content-Length: 0). So they pass the rest along without reading it. But the second person sees that there’s more and reads the next page as a whole new letter (Content-Length: 4). If you sneak in secret instructions on that second page, the second reader treats them as a new, valid message. That’s 0.CL request smuggling.
Reference reading: PortSwigger research on HTTP desync attacks and RFC 7230 for request framing rules.
3 - Recon: where I started
I began with basic recon in Burp Suite:
- Set my browser to proxy through Burp and opened the lab.
- Visited the homepage and a few static pages, recording all requests in HTTP history.
- Looked for unusual headers and values - especially
Content-Length
andTransfer-Encoding
.
What I found:
- Some responses hinted that static asset endpoints behaved differently.
- I spotted places where the app returned content after tiny requests (good sign for ERG selection later).
4 - Confirming the 0.CL behavior
Before building anything, I wanted to verify that the back-end would accept a smuggled request when Content-Length: 0
was used.
Manual check with Repeater (grouped test):
-
In Repeater, I composed:
(Note: the
GET /404
is placed after an empty POST body - intentionally.) -
I sent the whole combined unit and watched the response.
-
If the second request (GET /404) is processed unexpectedly (for example the server returns a 404 or an unexpected result), that indicates a 0.CL desync.
This quick Repeater test confirmed the lab’s 0.CL behavior for me.
Why this matters: I didn’t blindly jump to an exploit. First I checked that the back-end would indeed treat the following text as a new request.
5 - Finding the XSS injection point
The lab goal is to execute alert()
in Carlos’ browser. To solve the lab, I needed to find a place where I could inject JavaScript, and it would show up when Carlos visits the homepage. I searched the app for reflected data that would be rendered in the victim’s page:
- I tested common places: URL params, form fields, and headers.
- I observed that the User-Agent header was reflected in responses (it’s common in some apps to echo a header into HTML or into debug pages).
- I confirmed the reflection in Repeater by setting
User-Agent: a"/><script>alert(1)</script>
and checking if the returned HTML included the raw payload.
Important: I only used harmless alert(1)
payloads in the lab environment to avoid causing harm.
Result: The User-Agent
header was reflected back into the HTML, so it could be used to inject our XSS payload.
6 - Choosing the ERG (why endpoint choice matters)
I needed a reliable Early Response Gadget (ERG) — an endpoint that responds quickly and predictably. In testing, static resource paths (for example /resources/css/anything
) tended to provide stable, fast responses which made timing and ordering in the smuggling chain easier.
Why ERG matters:
- A fast ERG reduces timing uncertainty when you're queuing multiple requests over one connection.
- If an ERG is slow or varies, the smuggled request may not be processed the way you expect.
So I selected a static asset path as my carrier point (ERG), because it made the exploit more stable in repeated runs.
7 - Building the smuggling chain (Turbo Intruder)
Manual single-shot requests are flaky for 0.CL. I used Turbo Intruder to control raw TCP ordering and timing. Below is the structure I used — I’ll explain each piece in plain language.
Stage 1 (carrier request) — the front-end sees this first
%s
is a placeholder Turbo Intruder will fill so I can precisely control lengths.
Smuggled request (the secret we want the back-end to process)
- I put the XSS payload into the
User-Agent
header because I confirmed it’s reflected in the victim page. - The small body
x=1
is just to make the request well-formed.
Stage 2 (connection shaper) — keeps the flow predictable
- Some servers reject GETs with bodies, so I use
OPTIONS
or another tolerant method to avoid rejections and keep connection state stable.
Victim request — what Carlos will request
- This is the simple homepage request that Carlos requests periodically. If the smuggling worked, the XSS will be reflected when Carlos loads this page and his browser will execute
alert(1)
.
How Turbo Intruder runs them I queued Stage 1, then the Stage 2 + smuggled request, then the victim request. Turbo Intruder sends these in the same TCP stream so the back-end parses the smuggled GET as a distinct request while the front-end thinks it's body data.
8 - Running the attack and watching Carlos
I launched the Turbo Intruder script and watched the Burp/Turbo console:
- The script loops these sequences until it sees a success indicator (I set it to look for the lab “Congratulations” text or a page that contains the injected payload).
- Because Carlos refreshes every five seconds, I watched for a short while for the alert to appear.
Success signs I looked for:
- A response containing the injected string showing up in the victim page HTML.
- For PortSwigger labs, the “Congratulations” message or explicit lab success indicator.
When the injection reflected in the victim response, Carlos visited the homepage and the XSS ran — the lab marked it as solved automatically.
9 - Troubleshooting (what failed and why)
If it didn’t work right away, these are the common issues I debugged:
- Missing CRLFs or spacing: A single missing
\r\n
can break parser boundaries. I double-checked that each request ends with\r\n\r\n
. - Wrong ERG choice: Some endpoints are slower or proxied differently — try a few static asset paths.
- Server rejects GET with body: Replace
GET
body with anOPTIONS
or use a minimalContent-Length
on the smuggled GET with a tiny body. - Connection closing too early: Reduce concurrency, lower the rate, or add tiny delays (Turbo Intruder supports this).
- Turbo Intruder
%s
placement: Forgetting%s
in Stage 1 stops Turbo from calculating lengths — always confirm it’s present.
I iterated on these fixes until the XSS reflexed reliably.
10 - Defensive perspective (how to stop this)
This section is the most critical for defenders.
Code and config level fixes
- Ensure consistent HTTP parsing at the proxy and application layers. Terminate and re-encode requests at a single boundary when possible.
- Reject ambiguous requests — for example, requests that contain both
Content-Length
and Transfer-Encoding or oddContent-Length: 0
uses. - Upgrade and patch proxies (Nginx, HAProxy, Apache) and application servers to versions that address known desync bugs.
Monitoring & detection
- Log and alert when the body contains unexpected HTTP verbs (
GET
,POST
) or whenContent-Length
mismatch occurs. - Instrument front-end proxies to log raw request bytes for suspicious sequences.
- Use WAF rules to detect early response gadgets or repeated unusual patterns.
Developer tip: Add automated tests that send malformed requests. If front-end and back-end parse differently, your CI/CD test should fail.
11 - Final thoughts (what I learned)
Solving this lab taught me that:
- Small differences (a single
0
) can break server synchronization. - The practical exploit requires careful staging (ERG choice, request shaping, timing).
- Always think defensive — understanding the exploit is how you design the fix.
If you’re practicing this lab, take your time with the Repeater test, pick a stable ERG, and iterate on your Turbo Intruder script. When you see that alert()
in Carlos’ browser, you’ll have a better intuition for HTTP parsing than most developers.
References
- PortSwigger Web Security Academy – 0.CL lab
- James Kettle — HTTP Desync Attacks (PortSwigger research)
- RFC 7230 — HTTP/1.1 Message Syntax and Routing (for request framing details)

Herish Chaniyara
Web Application Penetration Tester (VAPT) | Security Researcher @HackerOne | PortSwigger Hall of Fame (#59) | Gold Microsoft Student Ambassador | Microsoft Certified (AI-900)