Hi Ben!

On Tue, Jan 20, 2026 at 04:29:44PM +0000, Ben Kallus wrote:
> Long post incoming!
> 
> Both nginx and haproxy do not care whether double-quoted strings are
> closed in chunk extensions; they just scan forward until hitting a
> line ending. As far as I can determine, this is not exploitable
> behavior with haproxy pointing at nginx or nginx pointing at haproxy
> in any configuration.

Yep!

> Thus, I believe Rajat is mistaken in two ways:
> 1. His claim that the RFC says that dquoted-CRLFs are allowable is
> wrong. The RFC says CR and LF cannot appear within a chunk-ext at all.
> 2. His claim that nginx allows dquoted-CRLFs in chunk-exts is wrong.
> Nginx does the same thing as haproxy.

Yes that's my observation as well, and I even built and installed
the same version as him (1.29.4) to verify that it wasn't a recent
change.

> Two questions remain to be answered:
> 1. Might this behavior be exploitable when haproxy is composed with
> some web server other than nginx?

No, because the chunks are removed on the input and recomposed on the
output (possibly of different sizes) and chunk extensions are entirely
lost. I was initially confused by Rajat's report because I didn't see
where the chunk-ext could have been stored to pass it on the other
side, and Christopher reminded me that I was just stupid, we indeed
entirely drop them.

> 2. Should haproxy change its current behavior?

IMHO it's totally useless. At best we could reject such cases, but
for no benefit except annoying owners of broken components (i.e. a
non-negligible portion of the net). Since here it's totally harmless,
better continue to ignore the extension like the spec suggests.

> First, question 1.
> 
> I believe that if there were a proxy server that did behave the way
> Rajat claimed that nginx does, you could do request smuggling if that
> server were forwarding requests to haproxy. (i.e., the inverse of the
> server chain Rajat provided).

No for the reason that we're doing what the spec suggests (like nginx
apparently).

> An example payload to exploit this discrepancy might look like this:
> (formatted for the hypothetical proxy's perspective)
> ```
> POST / HTTP/1.1\r\n
> Host: a\r\n
> Transfer-Encoding: chunked\r\n
> \r\n
> 0;ext="\r\n\r\nGET /evil HTTP/1.1\r\nX:"\r\n
> \r\n
> ```

But as you pointed this one is not valid as per the RFC. We could always
say that "what if..." but the we cannot prevent lenient parsers from
shooting themselves in the foot.

> (and now formatted for haproxy's perspective)
> ```
> POST / HTTP/1.1\r\n
> Host: a\r\n
> Transfer-Encoding: chunked\r\n
> \r\n
> 0;ext="\r\n
> \r\n
> GET /evil HTTP/1.1\r\n
> X:"\r\n
> \r\n
> ```

Yes exactly. Like any agent that will stop at the semi-colon like permitted
by the spec ("MUST ignore unknown extensions", and when you neither know nor
support any extension, you just ignore all of them).

> I dusted off the HTTP Garden to see if any suitable proxy server
> exists, and as far as I can tell, one doesn't. Pound is very, very
> close though. Pound will forward CRLFs inside of chunk-extensions, but
> won't forward 2 of them in a row. This is suitable to get trailer
> injection, but not enough for request smuggling, unless there's some
> creative thing I'm not thinking of :)

Maybe time to report it to them ;-) Because it will do the same with
about every other component since basically noone implements chunk-ext
in practice.

> To do the trailer injection, the payload looks like this for a `client
> <-> pound <-> haproxy <-> ...` server chain:
> Client sends this to pound: (text is formatted for pound's perspective)
> ```
> POST / HTTP/1.1\r\n
> Host: a\r\n
> Transfer-Encoding: chunked\r\n
> \r\n
> 0;ext="\r\nTrailer-not-interpreted-by-pound: value"\r\n
> \r\n
> ```
> Pound forwards this to haproxy: (text is formatted for haproxy's perspective)
> ```
> 'POST / HTTP/1.1\r\n
> Host: a\r\n
> Transfer-Encoding: chunked\r\n
> X-Forwarded-For: 172.18.0.1\r\n
> X-Forwarded-Proto: http\r\n
> X-Forwarded-Port: 80\r\n
> \r\n
> 0;ext="\r\n
> Trailer-not-interpreted-by-pound: value"\r\n
> \r\n'
> ```
> Haproxy then forwards this to the next server in the chain:
> ```
> POST / HTTP/1.1\r\n
> host: a\r\n
> transfer-encoding: chunked\r\n
> x-forwarded-for: 172.18.0.1\r\n
> x-forwarded-proto: http\r\n
> x-forwarded-port: 80\r\n
> \r\n
> 0\r\n
> trailer-not-interpreted-by-pound: value"\r\n
> \r\n
> ```
> 
> So I would say haproxy's behavior is indeed exploitable, but only when
> a proxy with a much more egregious spec violation (Pound) is between
> haproxy and the client.

Yes but again, IMHO it's spec-compliant agents that are exploitable
as long as they stop on the extension and ignore it without making
any particular effort to parse them.

> As for question 2, I lean toward fully validating chunk extensions. If
> we claim to implement a protocol, we should strive to do so completely
> and correctly when possible.

I disagree with this argument. We need to implement what we *use*. It's
even particularly dangerous to implement what you don't use because 1)
you can't test it, and 2) it's often done in too lenient a way to the
point that issues come from there. How many components have had issues
with trailers while they are unable to make use of them for example ?

> It seems unlikely to me that this would
> have an unacceptable performance cost, because almost no one uses
> chunk extensions anywway, so any validation code would probably run
> pretty infrequently. For 99% of requests, this should cost ~1
> predicted branch. Of course, theorizing about performance is super
> hard, so I could easily be wrong about the cost of the extra
> validation.

I'm definitely not worried about the performance cost here, just
adding more crap to parse something that will not be maintained over
time, that nobody will know how to test and that the spec clearly says
we must not even look at. Note that I have alreay written the code for
this (posted earlier in this thread), it's just that it's out of our
business since we're not supposed to read such extensions.

> Whether you think the spec requires this is I think this is a matter of taste.

Possibly :-)

> RFC 9112 says this:
> > When a server listening only for HTTP request messages, or processing what
> > appears from the start-line to be an HTTP request message, receives a
> > sequence of octets that does not match the HTTP-message grammar aside from
> > the robustness exceptions listed above, the server SHOULD respond with a 400
> > (Bad Request) response and close the connection.
> 
> It also says this:
> > A recipient MUST ignore unrecognized chunk extensions.
> 
> Certainly pound should be fixed though. I'll let them know.

That's exactly my point. I think that if you chain it to nginx (which
seems to do like haproxy), it will have the same problem.

Thanks!
Willy


Reply via email to