Detecting HTTP Request Smuggling Safely

Detecting HTTP Request Smuggling Safely

Written by

in

detecting HTTP request smuggling is less about firing a payload and more about proving a careful claim: that two servers in a chain really do slice one byte stream at different points, and that the gap between them is reachable and exploitable. A header that looks suspicious is not a finding. The finding is a desync you have confirmed with a safe, specific piece of evidence. This post walks the methodology we trust, and why the order of steps matters.

If the underlying mechanics are fuzzy, our primer on http request smuggling covers how a front end and a back end come to disagree about where a request ends. Here we assume you understand the mechanism and want to know how to confirm one in the wild without harming the people using the site.

Why detecting HTTP request smuggling is a confirmation problem

The reason this needs care is that the attack leaves no bad string to match. Every byte of a smuggling request is valid on its own. The vulnerability is entirely in how two parsers divide the same stream, which you cannot read off a single request or a single response. So detection cannot be a signature. It has to be an experiment: send something that behaves one way if the servers agree on boundaries and a different, observable way if they do not, then read the difference.

That framing also sets the bar for a report. A difference in how two servers echo a header is a hint, not proof. What proves a desync is a response that can only be explained by one server reading bytes the other server thought belonged to a different request. Everything below is about producing that response on purpose, safely.

Timing based probes

The safest first probe is timing based, because it can confirm a desync without ever leaving a smuggled request waiting for a real user. The idea is to craft a request that, if the back end parses it the way you suspect, will leave the back end waiting for more body bytes that never come. A back end that is waiting holds the response. You see a delay.

Consider a request whose front end reading finishes cleanly, but whose back end reading, under a Content-Length versus Transfer-Encoding disagreement, expects a further chunk:

POST / HTTP/1.1
Host: target.example
Transfer-Encoding: chunked
Content-Length: 4

1
A
X

If both servers agree, this returns promptly. If the back end honors Transfer-Encoding and is left expecting the next chunk after A, it waits for bytes that are not coming, and the response hangs until a timeout. A response that returns fast in the safe case and hangs in the desync case is a clean, self contained signal. Just as important, the probe is arranged so the bytes left over are consumed by the timeout, not parked in the buffer waiting to collide with the next visitor.

A good first probe for a desync is one that hangs on your own connection if it is real and bothers nobody if it is not. You are asking a question the server can only answer slowly.

Differential responses

Timing tells you something is off. Differential responses tell you what. The method is to send two requests that are identical except for the one detail you think triggers the desync, and compare what comes back. If a request crafted to smuggle a prefix produces a different status, a different error, or a different body than its harmless twin, and the difference matches your model of how the boundary moved, you are narrowing a hint into a mechanism.

The disciplined way to run this is to send the probe to yourself. Smuggle a prefix whose only effect is to change the response your own next request on that connection receives. If your follow up request comes back altered in exactly the way the smuggled prefix predicts, the desync is confirmed by your own traffic alone. You never needed a real user to complete the proof. An intercepting proxy that lets you control raw bytes and reuse a single connection is the tool for this; which one does not matter.

Careful teardown, so you do not poison real users

This is the step that separates responsible confirmation from reckless testing, and it is where most of the risk lives. A smuggle that works leaves a prefix in a shared back end buffer. If a real user’s request arrives before your own follow up, their request gets the prefix, and you have just affected someone who had nothing to do with your test. Detecting HTTP request smuggling responsibly means planning the cleanup before you send anything.

  • Prefer probes that self resolve. Timing probes that end in a timeout consume their own leftover bytes. They are the first choice precisely because there is nothing left to collide with another user.
  • Own both ends of the connection. When you must leave a prefix, send your completing request on the same connection immediately, so your own bytes claim the prefix and not a stranger’s.
  • Close the connection after a positive. Once a probe confirms a desync, close the connection so no residual state carries into later traffic on it.
  • Test gently and off peak. Fewer concurrent real requests means a far smaller chance of a collision during the brief window a prefix exists.

These same leftover bytes are what make outcomes like response queue poisoning dangerous, which is exactly why a tester has to treat them as live and clean them up rather than leaving them for chance. The care you take during detection is a smaller version of the care the fix is supposed to enforce.

Where confirmation beats guessing

The thread through all of this is that a desync is either real and reachable or it is noise, and only an experiment can tell the two apart. Flagging a Content-Length and Transfer-Encoding that look mismatched, then guessing at impact, produces a pile of maybes that waste a defender’s time. A timing probe that hangs, a differential response that matches your model, and a clean teardown together produce one thing worth sending: a confirmed, reachable desync with a safe proof attached. This confirmation first stance runs through our whole injection and input category.

Proving a desync, rather than reporting a suspicion, is the entire point of how UnboundCompute approaches a finding. It forms an idea about where a chain could break, designs an experiment that answers yes or no, and reports only once the evidence is in hand, which for a desync means a safe proof that the boundary really moved. That is the difference between signal and a guess, and it is what our about page describes.

Frequently asked questions

Why is detecting HTTP request smuggling a confirmation problem and not a signature?

The attack leaves no bad string to match, because every byte of a smuggling request is valid on its own. The vulnerability is in how two parsers divide the same stream, which you cannot read off a single request or response. So detection has to be an experiment: send something that behaves one way if the servers agree on boundaries and a different, observable way if they do not, then read the difference.

How does a timing based probe confirm a desync?

You craft a request whose front end reading finishes cleanly but whose back end reading leaves the back end expecting more body bytes that never arrive. A waiting back end holds the response, so you see a delay. A probe that returns fast when the servers agree and hangs until a timeout when they disagree is a clean, self contained signal, and the leftover bytes are consumed by the timeout rather than parked for the next visitor.

What are differential responses used for?

Timing tells you something is off; differential responses tell you what. You send two requests identical except for the detail you think triggers the desync and compare the results. The disciplined version smuggles a prefix that only changes the response your own next request on that connection receives, so a matching change confirms the desync from your own traffic alone, with no real user needed to complete the proof.

How do you test without poisoning real users?

Plan the cleanup before sending anything. Prefer timing probes that end in a timeout and consume their own leftover bytes. When you must leave a prefix, send your completing request on the same connection immediately so your own bytes claim it, close the connection after a positive so no residual state carries over, and test gently and off peak to shrink the window a prefix exists.


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.