The art of making proxies disagree

Introduction

My name is Victória Cesar, I am part of the security team at DigiF9, where I’ve been learning, researching, and occasionally convincing HTTP proxies to disagree with each other. My interest in the field started through security events and study groups, and eventually grew after some work involving intrusion detection in in-vehicle networks, which sparked my curiosity about protocols, networks, and the inner workings of the applications we use every day.

The research and experiments described here follow my process of learning, testing ideas, breaking implementations, and then trying to figure out what on earth actually happened afterwards. Most of the ideas came out of discussions with two colleagues and advisors: Felipe Brasileiro, Information Security Consultant, and Jodson Santos, MD of DigiF9. It was through this cycle of discussion, experimentation, and investigation that the work presented here gradually took shape.

 

The Art of Making Proxies Disagree

My journey as a security researcher started in the most traditional way possible: reading Request for Comments (RFCs) and somehow believing I’d understand everything straight away. RFCs are essentially instruction manuals published by the Internet Engineering Task Force, they define how a good portion of the internet is supposed to work. In theory, RFCs exist to organise chaos. In practice, they sometimes also reveal just how large the chaos actually is.

It was in this context that Jodson introduced me to RFC 9931, a document discussing security issues related to optimistic protocol transitions in HTTP/1.1 [1]. In other words, the client attempts to switch protocols during the connection and starts sending data in this new protocol before the server has even responded, simply assuming the transition will be accepted. It’s a bit like someone suddenly switching to Mandarin halfway through a conversation because they assumed you’d understand it. If the assumption is wrong, communication turns into confusion rather quickly. And once intermediaries are involved, this is exactly the sort of situation where request smuggling vulnerabilities begin to appear.At that point, however, concepts such as “request smuggling” and “HTTP intermediaries” may feel fairly abstract. After more internal conversations, I was pointed towards James Kettle’s research, and this led me to the article “HTTP/1.1 Must Die: The Desync Endgame”, written by the PortSwigger researcher [2]. While RFC 9931 focuses on vulnerabilities caused by optimistic protocol transitions, Kettle’s article presents a different perspective: desynchronisation as an inherent consequence of the protocol itself.

In HTTP/1.1, defining exactly where one request ends and the next begins is not always as straightforward as it probably should be. Every component in the infrastructure- proxy, cache, load balancer, or server- may interpret the request length differently. While one component believes the request has finished, another still thinks more data is arriving.

It is almost like two interpreters receiving the same continuous sequence of characters. One of them understands the message as a single word, while the other believes it should be divided into two separate words. Although both receive exactly the same characters, they arrive at different interpretations of the message.

 

Figure 1 - Different interpretations of the same continuous sequence of characters.

 

The article describes four main ways of defining where an HTTP request ends: Content-Length, Transfer-Encoding, Implicit-Zero, and HTTP/2’s built-in length. In the first approach, the Content-Length header explicitly defines how many bytes make up the request body. In the second, the Transfer-Encoding: chunked header indicates that the body will be sent in chunks, with the 0 marker indicating the end of the transmission. In the third approach, known as Implicit-Zero, the proxy or server simply assumes the request has no body at all. Finally, in HTTP/2, the message size is determined directly by the protocol’s binary frames.

 So when two components disagree about where a request ends, the famous desynchronisation is born. And wherever there is desynchronisation, there is room for request smuggling.

 From that point onwards, my advisors convinced me that if I truly wanted to understand both RFC 9931 and Kettle’s article, I’d need to go beyond theory. Reading about proxies, HTTP intermediaries, and request smuggling wasn’t enough. I needed to see everything working, or breaking, in practice.

With that goal in mind, I started developing a series of experimental implementations involving clients, servers, and different types of proxies. The first version of the environment consisted of an HTTP client, a forward proxy, a reverse proxy, and a server.

 

Figure 2 – Initial architecture of the experimental environment

 

At that stage, the proxies were doing pretty much the bare minimum required to deserve being called proxies: they received the request and forwarded exactly the same thing onwards, without performing any HTTP message processing whatsoever. On top of that, both the client and the server closed the connection after every request, preventing TCP connection reuse and, consequently, making request smuggling scenarios impossible. It is worth noting that up to this point, every implementation reflected a layperson’s interpretation of what proxies actually do. By the end of this article, however, all implementations will be based on RFC 9112 – Hypertext Transfer Protocol (HTTP).

It was almost like a conversation where each person says one sentence, hangs up immediately, and then calls back to reply. There simply isn’t enough time for messages to get mixed together or for someone to interpret one sentence as the continuation of another.

After that, I started developing implementations containing only a single intermediary proxy between client and server, alternating between the roles of security proxy, load balancer, and caching proxy. In these new versions, I implemented support for persistent connections (keep-alive), allowing multiple requests to reuse the same TCP connection. That was the moment when the tests finally started becoming interesting, because requests at last had the opportunity to interfere with one another, which was exactly the behaviour I wanted to study.

At this stage, the components were beginning to resemble real elements of a web infrastructure, although they were still extremely simplified versions. The security proxy was limited to identifying suspicious patterns such as path traversal attempts, SQL injection, and cross-site scripting. The load balancer merely alternated requests between two servers, without any particularly intelligent distribution logic. Meanwhile, the caching proxy stored previously received responses and avoided forwarding repeated requests to the backend.

In practice, however, I was still far from reproducing an environment complex enough for realistic request smuggling scenarios. The proxies fulfilled their roles only superficially and still failed to properly reflect the behaviour of real-world components. On top of that, my server mixed completely different concepts: the web server itself and the application running on top of it. Everything was so tangled together that the server received the HTTP request, interpreted the message, decided what to do with it, and responded almost as if it were having a conversation with itself.

It was at that point that my advisors suggested I study existing implementations of simple HTTP servers to better understand the separation between server and application logic. Among the chosen libraries were Python’s HTTPServer module [3] and the WEBrick library [4].

While analysing HTTPServer, I noticed a much clearer separation between the server and the application. The server was responsible for parsing the HTTP request, extracting the method, path, headers, and other relevant information. The application, on the other hand, simply received that information and decided how to respond.

Despite this, HTTPServer proved unsuitable for request smuggling experiments. Its limitations included restricted support for methods such as GET and HEAD, as well as the fact that it ignored request bodies entirely in every scenario. Which unfortunately made it rather difficult to study ambiguities related to HTTP message lengths. It’s hard to trigger desynchronisation when the server looks at the request body and effectively decides it’s none of its business.

In theory, I could simply modify the logic on one side to force different interpretations. The problem was that, at that stage of the research, I didn’t just want to trigger the vulnerability, I needed to understand exactly why it happened. And doing that by analysing WEBrick’s internal behaviour was beginning to look more complicated than rebuilding my own implementations from scratch.

For that reason, my advisors suggested rebuilding my implementations of the client, server, security proxy, caching proxy, and load balancer, this time attempting to make them as close as possible to RFC compliant HTTP components. My main reference became RFC 9112, which defines much of HTTP/1.1 behaviour and influences widely used implementations such as Nginx and Apache HTTP Server, providing a clearer distinction between expected behaviour and questionable implementation decisions [5].

The first step involved implementing an HTTP server that behaved more like an RFC compliant component. Its characteristics included always responding using HTTP/1.1 with a Content-Length header, interpreting messages without Content-Length or Transfer-Encoding as bodyless requests, and exclusively accepting the chunked value for the Transfer-Encoding header.

However, in order to allow request smuggling experiments, I deliberately had to ignore part of the behaviour prescribed by the RFC for requests containing both the Transfer-Encoding and Content-Length headers. Although the specification allows a server to process such requests by considering only the Transfer-Encoding header, it also requires the connection to be closed after sending the response in order to mitigate potential attacks. In other words, after spending several pages studying what should be done, I eventually reached the point in the research where I had to implement precisely the opposite, keeping the connection open to enable the experiments.

At the next stage, I developed a reverse proxy that would serve as the foundation for the security proxy, caching proxy, and load balancer. Its features included removing hop-by-hop headers as well as all headers explicitly listed within the Connection header, following the recommendations of the HTTP/1.1 RFCs.

 

Figure 3 – RFC compliant architecture used during the experiments

 

To enable controlled request smuggling tests, I also implemented a dedicated testing flag. When enabled, it caused the proxy to propagate both Transfer-Encoding and Content-Length simultaneously over the upstream connection, behaviour that directly contradicts the RFC recommendations, but was necessary to reproduce CL.TE style attacks. Once this flag was enabled, the entire infrastructure gradually lost the ability to reach a simple consensus about where the request actually ended.

In this scenario, the request first passed through the security proxy and the caching proxy, both of which used Content-Length to determine the message size. Afterwards, the load balancer interpreted the exact same request using Transfer-Encoding.

 

Figure 4 – Desynchronisation scenario involving simultaneous propagation of Content-Length and Transfer-Encoding

 

The result was exactly the sort of behaviour I had been trying to reproduce since the beginning of the research: while some components believed the request had already finished, others still believed more data was arriving. Part of the payload ended up being interpreted as an entirely separate HTTP request. In the end, one proxy thought the conversation was over while another was replying to something nobody had actually asked. That was where the desynchronisation happened.

 

Conclusion

Throughout the research, I realised that understanding request smuggling goes far beyond copying ready made payloads from the internet. It required understanding how proxies, caches, load balancers, and servers actually interpret an HTTP request, and how small differences in that process can generate serious vulnerabilities. Implementing clients, proxies, and servers also helped transform several concepts that previously sounded like nothing more than complicated RFC terminology into real observable behaviour during testing.

In the end, the entire research project revolved around one rather simple conclusion: HTTP/1.1 works perfectly well right up until the moment when two components start disagreeing about where a request ends. And apparently, convincing different proxies to interpret the same message differently requires far less effort than I originally expected.

This article does not necessarily introduce anything new to the cybersecurity community, but its goal has always been to make the vulnerability easier to understand by going beyond simply executing payloads. I hope you enjoyed it and that the reasoning presented here contributes to a better understanding of the vulnerability. The source code developed throughout this research is available in the project repository [6].

 

References

[1]SCHWARTZ, Benjamin M. Security Considerations for Optimistic Protocol Transitions in HTTP/1.1. Fremont: RFC Editor, 2026. RFC 9931. Available at: https://www.rfc-editor.org/rfc/rfc9931.html. Accessed: June 1, 2026.

[2]KETTLE, James. HTTP/1.1 must die: the desync endgame. PortSwigger Research, 2025. Available at: https://portswigger.net/research/http1-must-die. Accessed: June 1, 2026.

[3]PYTHON SOFTWARE FOUNDATION. server.py. In: cpython. [S. l.], 2026. Available at: https://github.com/python/cpython/blob/main/Lib/http/server.py. Accessed: June 1, 2026.

[4]RUBY COMMUNITY. webrick. GitHub, 2026. Available at: https://github.com/ruby/webrick. Accessed: June 1, 2026.

[5]FIELDING, Roy; NOTTINGHAM, Mark; RESCHKE, Julian. HTTP/1.1. RFC Editor, 2022. RFC 9112. Available at: https://www.rfc-editor.org/rfc/rfc9112.html. Accessed: June 1, 2026.

[6]FIGUEIREDO, Victória Cesar. http-request-smuggling-lab. GitHub, 2026. Available at: https://github.com/victoriacfigueiredo/http-request-smuggling-lab. Accessed: June 1, 2026.

 

arrow_back Back to blog
Share

Ready to strengthen your security posture?

Talk to DigiF9 about secure software, AppSec, and cyber resilience.

Contact Us arrow_forward