Introduction: The Invisible “Layer”
In modern web architecture, an HTTP request from a user often passes through multiple checkpoints before reaching the actual business logic (backend server): CDN, load balancer, WAF, reverse proxy…
We can imagine this model as a tightly secured building:
- Frontend Proxy (e.g., Nginx, CDN): This is your building’s “security guard”. He stands at the entrance, responsible for initial security checks, ensuring that visitors (requests) comply with regulations.
- Backend Server (e.g., Tomcat, Node.js): This is your “butler”. He provides the actual services (business logic) based on the visitors allowed in by the security guard.
Under normal circumstances, the security guard and the butler use the same “visitor manual” (HTTP protocol standard). After the security guard checks, the butler confirms again, creating a seamless process.
However, HTTP Request Smuggling (HRS) exploits the fact that the security guard and the butler are using two “different versions of the visitor manual”. They have different interpretations of the “boundaries” of the same visitor (the same HTTP message).
The attacker takes advantage of this “understanding difference” to hide a “malicious visitor” in the backpack of a “normal visitor” that the security guard has allowed in. The security guard does not see it, but the butler treats it as a separate visitor, leading to a trust crisis.
Core: The Conflict of Two “Visitor Manuals”
The HTTP protocol historically defines two main ways to determine where the “end” of a request is:
- Content-Length (CL): Like a “list of items”. It clearly tells the receiver: “I have exactly 50 bytes of data in my backpack.” The receiver will strictly read according to this number.
- Transfer-Encoding: chunked (TE): Like “chunked packaging”. It tells the receiver: “My luggage is delivered in batches, and before each batch, I will tell you how big it is. When you receive a batch of size
<span>0</span>, it means everything is finished.”
The root of the conflict: What happens when a request contains both headers?
- RFC 2616 (old standard) stipulated that if both
<span>TE</span>and<span>CL</span>exist simultaneously,<span>TE</span>takes precedence and<span>CL</span>should be ignored. - However, the software implementations in the real world vary widely. Some “security guards” strictly adhere to this rule, while some “butlers” may only recognize
<span>Content-Length</span>due to configuration or outdated versions. This is where the “desynchronization” begins.
Attack Scenarios: TE.CL vs. CL.TE
Attackers aim to exploit this desynchronization to “smuggle goods”.
Scenario One: CL.TE (Guard believes CL, Butler believes TE)
This is the most straightforward “hiding” technique. The attacker carefully constructs a backpack (HTTP request):
- Attacker sends:
POST /login HTTP/1.1 Host: example.com Content-Length: 55 <-- Guard sees this Transfer-Encoding: chunked <-- Butler sees this 10 <-- Telling the Butler, there are 16 bytes below username=admin&pass= 0 <-- Telling the Butler, my "normal request" ends here GET /admin/delete?user=bob HTTP/1.1 Host: example.com - View from the “Guard” (Frontend Proxy) (CL):
- He only recognizes
<span>Content-Length: 55</span>. - He starts counting 55 bytes from the request body. After counting
<span>...user=bob</span>and part of the following (depending on the exact length), he thinks, “Oh, a complete visitor, let him in.” - He forwards these 55 bytes intact and unmodified to the butler.
- First chunk:
<span>10</span>(16 bytes), content is<span>username=admin&pass=</span>. - Second chunk:
<span>0</span>(end chunk).
- He follows the “TE takes precedence” rule and only recognizes
<span>Transfer-Encoding: chunked</span>. - He starts parsing the chunk:
- Key Point: The butler considers the request legitimately finished after receiving
<span>0</span>. - However, at this point, there is still “extra” data floating in the TCP connection that the guard forwarded, within the
<span>CL</span>length:<span>GET /admin/delete...</span>.
<span>GET /admin/delete...</span><code><span> request gets executed.</span>Scenario Two: TE.CL (Guard believes TE, Butler believes CL)
This is another more common “sticky” technique.
- Attacker sends:
POST /login HTTP/1.1 Host: example.com Transfer-Encoding: chunked <-- Guard sees this Content-Length: 4 <-- Butler sees this 5c <-- Telling the Guard, there are 92 bytes below GET /internal/secret HTTP/1.1 Host: internal.host X-Ignore-Me: 0 <-- Telling the Guard, my request ends here - View from the “Guard” (Frontend Proxy) (TE):
- First chunk:
<span>5c</span>(92 bytes), content is<span>GET /internal/secret...</span>. - Second chunk:
<span>0</span>(end chunk).
- He only recognizes
<span>TE: chunked</span>. - He parses the chunk:
- He considers this a complete, legitimate request and forwards it intact to the butler.
- He does not recognize
<span>TE</span>(or is configured to ignore it), only recognizes<span>Content-Length: 4</span>. - He starts reading 4 bytes from the request body, which is
<span>5c.
</span> - Key Point: After reading 4 bytes, he considers this
<span>POST /login</span>request to be legitimately finished. - At this point, there is still “extra” data floating in the TCP connection that the guard forwarded, within the
<span>TE</span>chunk:<span>GET /internal/secret...</span>.
<span>GET /home ...</span>) is sent to the same TCP connection. The butler will combine this “extra” data with the victim’s request. What the butler actually receives could be: <span>GET /internal/secret ... X-Ignore-Me: GET /home ...</span>. This could lead to the victim’s request failing (DoS), or even attaching the victim’s cookie to the malicious request.The Dangers of “Smuggled Goods”: From “Pranks” to “Nuclear Weapons”
When an attacker successfully smuggles a request in the “layer”, what can they do?
-
Web Cache Poisoning (Most Dangerous) This is an amplifier of HRS harm. The response to the malicious request smuggled by the attacker is incorrectly cached by the frontend proxy (CDN/cache) as the response to the next normal request. Subsequently, all normal users accessing that resource will receive polluted sensitive or malicious data.
-
Bypassing Frontend Access Control The “guard” is responsible for checking permissions (e.g., prohibiting external access to the
<span>/admin</span>interface). By smuggling a<span>GET /admin</span>request, the attacker perfectly bypasses the “guard’s” checks, allowing the “butler” to execute requests that should be prohibited directly on the internal network. -
Session Hijacking and Request Forgery By “sticking” to the victim’s request, the attacker can attach the victim’s cookie to their own crafted malicious request header, thus executing operations as the victim.
Defense: How to Unify the “Manuals” of the “Guard” and the “Butler”?
The essence of HRS is “parsing desynchronization”. Therefore, the core of defense is “enforced synchronization”.
-
Request Normalization (Best Practice, Root Cause Treatment) Make your “guard” (frontend proxy) a “transit station” rather than a “telephone”. After receiving a request, the guard should not forward it unchanged, but should fully parse the request, then regenerate a brand new, clean HTTP request to send to the backend “butler”. During this “regeneration” process, all ambiguous and conflicting headers (such as the simultaneous presence of
<span>CL</span>and<span>TE</span>) should be discarded or normalized, thus eliminating the possibility of smuggling. -
Unified Standards and Upgraded Components Ensure that your CDN, load balancer, WAF, reverse proxy, and backend servers are all up to date, with more consistent implementations of RFC 7230 (new standard).
-
Reject Ambiguous Requests (Simple and Direct) Configure rules at the frontend proxy (guard) layer:
if (Header contains BOTH Content-Length AND Transfer-Encoding) { return 400 Bad Request; // Directly reject }This can block the vast majority of known HRS attack patterns.
-
Disable Connection Reuse (Performance for Security) When the proxy forwards to the backend, force set
<span>Connection: close</span>. This forces each HTTP request to use a brand new TCP connection, greatly increasing the difficulty of “sticking” and “smuggling”. However, this comes with significant performance overhead.
Conclusion 💡
HTTP request smuggling is a “protocol layer” attack that exploits not vulnerabilities in your code, but cracks in your architecture. In an increasingly complex microservices and cloud-native architecture, a request may pass through 4 or 5 “hops”, each of which could be a “desynchronized” “guard”.
As security engineers, we must ensure that all components in the architecture refer to the same “visitor manual”, establishing an unbreakable trust chain from the ground up.