Challenge Overview#
- Challenge name: Client-side desync
- Category: HTTP Request Smuggling
- Level: Expert
- Objective: Bypass cross-origin protections and capture the victim user’s session cookie using client-side desync, then use it to impersonate the victim and solve the lab.
1. Fundamental Concepts#
1.1 The Paradigm Shift: From Server-Side to Client-Side Desync#
Traditional HTTP Request Smuggling operates on the boundary between two back-end infrastructure components: a Front-end Reverse Proxy and a Back-end Origin Server. It exploits parsing discrepancies (e.g., CL.TE or TE.CL) over a shared connection pool.
In contrast, Client-Side Desync (CSD), pioneered by PortSwigger Research (James Kettle, 2022), shifts the attack surface entirely to the Client-to-Server connection:
| |
Even if an application employs no front-end reverse proxy, disables server-side connection pooling, or runs on isolated microservices, it remains inherently vulnerable if the server exhibits desynchronization behaviors directly toward client browsers.
1.2 Browser Connection Pooling (Keep-Alive)#
All modern web browsers (Google Chrome, Mozilla Firefox, Apple Safari, Microsoft Edge) implement connection pooling over HTTP/1.1 for performance optimization.
- Rather than establishing a new TCP 3-way handshake and TLS negotiation for every individual asset, the browser maintains a pool of persistent, reusable Keep-Alive sockets bound to a specific origin (
https://target.com). - When multiple HTTP requests are dispatched to the same origin—even across cross-origin contexts—they are pipelined sequentially over the same underlying TCP stream.
1.3 The CL.0 Vulnerability (Content-Length: 0)#
According to RFC 7230 / RFC 9112, if an HTTP/1.1 request contains a Content-Length: X header, the receiving server must consume exactly $X$ bytes from the stream before processing subsequent requests.
However, many web servers, load balancers, and application frameworks (e.g., Apache, Nginx, Node.js, Python WSGI) implement non-compliant optimization shortcuts on specific routes:
- Redirect Endpoints (301/302) & Static Resources: When a client issues a
POSTrequest to an endpoint primarily designed to redirect (such as/redirecting to/en), the server presumes that redirects do not require a request body. - The server parses the headers, immediately emits an
HTTP/1.1 302 Foundresponse, and fails to read the body bytes off the socket buffer. - This constitutes a CL.0 condition: The browser assumes the server will read the body (governed by
Content-Length), while the server treats the request body as non-existent (Content-Length = 0).
2. Attack Model & Architecture#
2.1 The Vulnerability Mechanics#
When a server ignores the Content-Length on a POST / request, the unread body data remains stranded in the operating system’s TCP socket receive buffer.
When the browser sends its next request over that same persistent socket:
- The server reads from the socket buffer and encounters the leftover bytes from the previous request’s unread body.
- The server interprets those leftover bytes as the beginning (the prefix) of the new incoming request.
- The real second request is appended to this prefix, effectively concatenating the two requests together on the server.
2.2 Attack Workflow & Data Flow Diagram#
| |
2.3 The Three Tactical Pillars of CSD#
1. The CORS Failure Trigger (mode: 'cors' + .catch())#
- If a cross-origin
fetch()receives an HTTP 302 redirect, standard browser behavior is to follow the redirect transparently. Following the redirect would waste the desynchronized socket. - By setting
mode: 'cors', the browser demands theAccess-Control-Allow-Originheader. Because the target server does not supply it, the browser halts immediately and throws a DOMException (CORS error). - The attacker attaches a
.catch()handler to this failure. The moment the CORS error is caught, the script fires the secondary request down the same open socket before the browser closes it.
2. Automatic Credential Inclusion (credentials: 'include')#
- When the secondary request (
/capture-me) is triggered, the browser automatically attaches all ambient credentials associated with the target origin—including sensitivesessioncookies andHttpOnlyflags.
3. The Stored Reflection Gadget (Comment Storage Sink)#
- To exfiltrate the victim’s session cookie without violating cross-origin read restrictions, the attacker directs the smuggled prefix to the blog’s comment endpoint (
POST /en/post/comment). - The parameter
comment=is left open-ended at the end of the smuggled prefix with an inflatedContent-Length: 900. - The entire subsequent request—including the victim’s private cookies—is consumed as the text content of the comment and publicly rendered on the blog post.
3. Step-by-Step Exploitation (PoC)#
3.1. Phase 1: Identifying the CL.0 Endpoint in Burp Suite#
- Capture a baseline request
GET /in Burp Suite and send it to Repeater. - Notice that requesting
/returns an immediateHTTP/1.1 302 Foundredirecting to/en:
| |

- In Burp Repeater, open the tab options menu (gear icon) and uncheck Update Content-Length.
- Change the request method to
POST / HTTP/1.1, set an arbitrary headerContent-Length: 10, but leave the request body completely empty:

- Send the request. The server replies instantaneously with
302 Foundrather than hanging or timing out while waiting for 10 bytes of body data.Diagnostic: The server completely ignores
Content-Lengthon the/endpoint, confirming a CL.0 desync vector.
3.2. Phase 2: Confirming Desynchronization in Burp Suite (Single Connection Sequence)#
Before involving a browser, we prove that socket desynchronization can be achieved over a single TCP connection.
- In Burp Repeater, create Tab 11 containing the malicious request with a smuggled prefix targeting
/404:
| |

- Create Tab 12 containing an ordinary request:
| |
- Group both tabs into a tab group named
test. - In the drop-down menu next to the Send button, choose Send group (single connection).
- Send the sequence. Tab 11 receives the expected
302 Found, while Tab 12 receives:
| |

Conclusion: The server processed the unread body of Tab 11 as the prefix of Tab 12, routing the second request to
/404.
3.3. Phase 3: Replicating the Desync Vector in a Clean Browser#
A critical methodology step in Client-Side Desync is verifying that a real browser engine (with native networking and security constraints) exhibits the exact same socket reuse behavior.
CRITICAL METHODOLOGY NOTE: In this step, disable Burp Proxy (FoxyProxy) or use a dedicated clean browser profile.
- Why? If proxied through Burp, the browser’s TCP socket terminates at
127.0.0.1:8080. Burp Proxy acts as a man-in-the-middle, decoupling and normalizing connections to the upstream server. Testing without proxy ensures the browser communicates directly with the target socket over real-world Keep-Alive connections.
- Open a clean Google Chrome browser window directly accessing the target lab.
- Open Developer Tools (
F12), navigate to the Console tab, and execute:
| |

- Switch to the Network tab (ensure Preserve log is enabled).
- Verify the traffic:
- The first
POST /returns302and triggers a CORS failure, entering the.catch()block. - The second request fires on the same socket and returns
404 Not Found:
- The first

Verification Complete: The browser successfully reused the desynchronized socket to execute the smuggled request.
3.4. Phase 4: Weaponizing the Comment Storage Sink#
To capture the victim’s session cookie, we chain the desync vector with the application’s blog comment feature.
- Re-enable the proxy, visit blog post 7 (
/en/post?postId=7), and submit a test comment. - In Burp Proxy > HTTP history, inspect the comment submission request:
| |

- Note the critical parameters:
- Target URL:
/en/post/comment - Valid Anti-CSRF Token:
csrf=knEeHj3xsIS7zvDRJTWcbnhGl4GRJq2F - Target Post:
postId=7 - Attacker’s active session cookie and analytics token.
- Target URL:
3.5. Phase 5: Crafting & Delivering the Exploit Payload#
- Navigate to the Exploit Server (
/exploit). - In the Body section, configure the complete exploit payload:
- The outer request targets the vulnerable CL.0 endpoint (
POST /). - The body injects a complete
POST /en/post/commentrequest. - An inflated
Content-Length: 900is defined. - The parameter
comment=is positioned at the very end of the body. - When the victim’s browser sends the subsequent
fetch('/capture-me'), the server appends the entire incoming request (headers + cookies) into thecommentfield.
- The outer request targets the vulnerable CL.0 endpoint (
| |

- Click Store, then click Deliver exploit to victim.
3.6. Phase 6: Harvesting the Session Cookie & Account Takeover#
- Return to blog post 7 (
/en/post?postId=7) and refresh the page. - Under the comment section, a new comment submitted by
victim_hackerappears. The comment body exposes the victim browser’s raw HTTP request:
| |

- Extract the stolen session cookie:
| |
- In Burp Repeater, send a request to
/or/my-accountwith the victim’s session cookie replacing your own:

- Refresh the browser. The lab banner updates to Solved.

4. Remediation & Defense Strategies#
4.1 Strict RFC 7230 / RFC 9112 Conformance for Message Bodies#
The fundamental flaw enabling CL.0 is the server prematurely concluding request processing without draining the socket.
- Drain Socket Streams: If an incoming request specifies
Content-Length > 0, the web server must read and discard the entire body payload before emitting a response (even for 3xx redirects or 4xx/5xx errors). - Connection Termination on Unexpected Bodies: If an endpoint does not support request bodies (such as
GETor redirect endpoints), the server must reject the request with400 Bad Request, issueConnection: close, and immediately tear down the TCP socket to prevent pipeline reuse.
4.2 Upgrading to End-to-End HTTP/2#
- Client-Side Desync fundamentally relies on HTTP/1.1 pipelining over a persistent Keep-Alive TCP stream.
- Under HTTP/2, requests are multiplexed into distinct, independent streams (
Stream ID). Bytes belonging to Stream 1 can never bleed into or prefix Stream 2. Enforcing HTTP/2 directly between client browsers and edge servers eliminates CSD entirely.
4.3 Robust Cookie Hardening (SameSite Attribute)#
While CSD originates as a request framing bug, the primary objective in this attack chain is stealing ambient credentials.
- Enforce
SameSite=LaxorSameSite=Stricton all sensitive session identifiers:
| |
- Under
SameSite=LaxorStrict, the browser strictly suppresses cookies on cross-originPOSTfetch requests initiated by third-party origins, preventing the victim’s authentication context from being captured during desynchronization.
4.4 Web Server Hardening Examples#
Node.js / Express#
Ensure middleware explicitly reads incoming streams or rejects bodies on redirect routes:
| |
Nginx#
Ensure Nginx strictly enforces body reading before processing redirects:
| |
