Hi,
When OpenBSD httpd is configured to use FastCGI, and it receives a
request with a chunked message body, it echoes the request body as a
prefix to the response. Presumably, this issue breaks all chunked
POSTs to OpenBSD httpd instances that use FastCGI, so I'm not quite
sure how it hasn't been noticed by now.
The problem is in server_read_httpchunks. There's a sequence of calls
to server_bufferevent_write_chunk and server_bufferevent_print that
cause the request body to be written to the response socket before the
fcgi code runs.
The interesting part of this comes from the fact that the intersection
between what gets through httpd's chunked body parser and what gets
through a browser's response parser is nonempty :)
For example, Chrome and Firefox ignore numeric prefixes on HTTP
versions in response-lines, so "64HTTP/1.1 200 OK" is valid to those
browsers. Correspondingly, httpd's chunked message body parser treats
that line as a valid chunk-size line, so you can send fun requests
like this:
```
POST / HTTP/1.1\r\n
Host: a\r\n
Transfer-Encoding: chunked\r\n
\r\n
64HTTP/1.1 200 OK\r\n
Content-Length: 39\r\n
\r\n
<script>alert("hello world")</script>\r\n
```
...and get the following back from httpd:
```
64HTTP/1.1 200 OK\r\n
Content-Length: 39\r\n
\r\n
<script>alert("hello world")</script>\r\n
HTTP/1.1 200 OK\r\n
(the rest of the server's typical response goes here)
```
If you can get a browser to send that request (or something like it)
to an httpd with FastCGI enabled, it'll pop an alert. This is because
the stuff after the request headers looks enough like a chunked
message body to get through httpd's parser, and *also* looks enough
like an HTTP response to get through Firefox and Chromium's response
parsers.
Of course, users being able to execute code in their own browsers is
not interesting. What's interesting is users executing code in others'
browsers :) In principle, this bug could allow for this too, if there
was a "perfect storm" HTTP cache in front of httpd. This cache would
have to both
- let through the request payload without too much normalization, and
- parse responses permissively enough to not reject the weird response
that httpd sends back, and store it in the cache.
I don't have tooling to easily test whether any popular web caches fit
the bill here.
Reproduction steps for -current:
0. Copy the following into /var/www/htdocs/index.php: (you could use
any programming language you like, but PHP is convenient because of
fpm.)
```
<?php
echo "hello world";
?>
```
1. Copy over a simple httpd.conf that uses FastCGI:
```
prefork 1
server "default" {
no log
listen on * port 80
root "/htdocs"
fastcgi {
param SCRIPT_FILENAME "/htdocs/index.php"
socket "/run/php-fpm.sock"
}
}
```
2. Install php, git, and gdb:
```
pkg_add php-8.4.1 git gdb
```
3. Build httpd from source, for ease of debugging:
```
git clone --depth 1 https://github.com/openbsd/src \
&& cd usr.sbin/httpd \
&& make \
&& clang -O0 -ggdb -static *.c /usr/lib/libevent.a /usr/lib/libtls.a
/usr/lib/libssl.a /usr/lib/libcrypto.a /usr/lib/libutil.a -o httpd
```
4. Start httpd and php-fpm:
```
./httpd && php-fpm-8.4
```
5. Attach gdb to the running server, set a conditional breakpoint for
writes of size 5, and continue:
```
egdb -p "$(ps aux | grep 'httpd: server' | awk '{print $2}')" httpd
(gdb) b write if $rdx == 5
(gdb) c
```
6. From another shell, send an HTTP request that uses the chunked
transfer-encoding:
```
printf 'POST / HTTP/1.1\r\nHost: whatever\r\nTransfer-Encoding:
chunked\r\n\r\n0\r\n\r\n' | nc localhost 80
```
7. You should hit the breakpoint in GDB. If you run `finish`, you'll
see that the message body from the request ("0\r\n\r\n") pops out from
the `nc` in step 6.
-Ben