Duplicate Content Length Smuggling

Duplicate Content Length Smuggling

Written by

in

duplicate content length smuggling is a desync that does not need Transfer-Encoding at all. It happens when one request carries two Content-Length headers with different values. The standard says a server must reject that outright, but not every server does. Some take the first value, some take the last, and the moment two servers in a chain make opposite choices they disagree about where the body ends. That is the same split that drives every other form of http request smuggling, reached through one duplicated header instead of two competing ones.

How duplicate content length smuggling creates a split

Every request with a body needs to tell the server how many bytes to read. Normally one Content-Length header does that job. The attack puts in two, with different numbers, and lets the front end and the back end each pick whichever one they prefer. If the front end picks the larger value and the back end picks the smaller, the front end reads and forwards more bytes than the back end will consume, and the extra bytes become a request the attacker wrote.

Here is a request with two length headers:

POST / HTTP/1.1
Host: acme-notes.example
Content-Length: 53
Content-Length: 0

GET /admin HTTP/1.1
Host: acme-notes.example
Foo: x

Read it as the front end, which here honors the first header, Content-Length: 53. It reads 53 bytes of body after the blank line, which is the whole GET /admin block, and forwards everything as one POST. Read it as the back end, which here honors the last header, Content-Length: 0. It decides the body is empty and the POST ends at the blank line. The 53 bytes the front end thought were a body are still in the back end buffer:

GET /admin HTTP/1.1
Host: acme-notes.example
Foo: x

Those bytes become the next request on the connection. The dangling Foo: x with no finished value swallows the first line of the next victim request, so the back end runs the attacker GET /admin stitched onto the victim headers, cookies and all. Flip which server reads which header, start the first value at zero and the last at 53, and the split still works. The only requirement is that the two servers disagree.

The standard is unambiguous: reject a request that carries two different Content-Length values. The bug is every server that decides to be helpful and picks one instead.

Why servers disagree even without Transfer Encoding

RFC 9112 is direct about this. If a message has more than one Content-Length header, or a single header with a list of different values, the server must treat the message as unrecoverable and reject it. The rule exists precisely because duplicate lengths are a smuggling vector. The gap is between the rule and the field. Real servers have historically handled the duplicate in several ways: use the first, use the last, use the largest, reject the message, or stop at the first value and ignore the rest. None of those is safe if the server next to it in the chain chose differently.

This variant is in some ways simpler to trigger than the chunked ones, because it needs no Transfer-Encoding support anywhere. There is no chunk size to compute and no hexadecimal math. You are relying purely on two plain headers and the chance that two parsers resolve the conflict in opposite directions. That makes it a good thing to check for early, and a clean illustration of why a request that looks valid to each server on its own can still be read two ways across the pair. If you are putting together a test plan, it pairs naturally with the techniques in detecting http request smuggling.

It rhymes with the duplicate Transfer Encoding trick

Sending two of the same header to split two parsers is the same mindset behind one branch of TE.TE request smuggling, where a second Transfer-Encoding header with a junk value makes one server ignore the encoding. Duplicate Content-Length is the blunter cousin: no obfuscation, just two honest looking numbers that happen to contradict each other. In both cases the attacker is not sending anything malformed in an obvious way. Each header is well formed. The contradiction only appears when you read the request as the whole chain reads it.

Defenses that hold

The defense is the one the standard already states: reject the message. A request with two different Content-Length values should get a single error response and a closed connection, never a best guess. Closing the connection is the part that matters, because a leftover prefix has nowhere to wait once the socket is gone. The front end should also normalize every request into one unambiguous framing, with a single length header it computed itself, before it forwards anything to the back end, so the back end is never handed a contradiction to resolve. These are parser discipline problems at heart, which is why we keep them under injection and input alongside the other ways untrusted bytes get read wrong.

A scanner can flag that a request carried two Content-Length headers. What it cannot tell you is whether the specific front end and back end in your path resolve that duplicate in opposite directions, which is the only thing that turns a rejected looking request into a live desync. Answering that means reasoning about how each real parser in the chain behaves and proving the request boundary actually moves, not matching a pattern. That is the work UnboundCompute is built for: deciding whether a desync is genuinely reachable rather than pattern matching headers, which you can read about on our about page.

Frequently asked questions

What is duplicate content length smuggling?

It is a request desync caused by sending one request with two Content Length headers that hold different values. The standard requires a server to reject such a message, but some servers instead pick a value. When the front end picks one value and the back end picks the other, they disagree about where the body ends, and the extra bytes become a request the attacker hid inside the first one.

Does this attack need Transfer Encoding?

No. That is what sets it apart from the chunked variants. It relies only on two plain Content Length headers and the chance that two parsers in the chain resolve the contradiction in opposite directions. There is no chunk size to compute and no hexadecimal math, which makes it simpler to trigger and a good thing to check for early in a test plan.

What does the RFC say about two Content Length headers?

RFC 9112 is direct: if a message has more than one Content Length header, or a single header listing different values, the server must treat the message as unrecoverable and reject it. The rule exists because duplicate lengths are a known smuggling vector. The problem is that real servers have historically used the first value, the last value, the largest, or rejected outright, and any mismatch across the chain is exploitable.

How do you prevent duplicate content length smuggling?

Follow the standard and reject the message. A request with two different Content Length values should get one error response and a closed connection, never a best guess, because closing the socket leaves a leftover prefix nowhere to wait. The front end should also normalize every request into a single unambiguous framing with one length header it computed itself before forwarding, so the back end never has to resolve a contradiction.


Put an autonomous researcher on your own systems

UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.