CVE-2022-24766 (Python, published 2022-03-21T19:15Z)
Description: value: mitmproxy is an interactive, SSL/TLS-capable intercepting proxy. In mitmproxy 7.0.4 and below, a malicious client or server is able to perform HTTP request smuggling attacks through mitmproxy. This means that a malicious client/server could smuggle a request/response through mitmproxy as part of another request/response's HTTP message body. While mitmproxy would only see one request, the target server would see multiple requests. A smuggled request is still captured as part of another request's body, but it does not appear in the request list and does not go through the usual mitmproxy event hooks, where users may have implemented custom access control checks or input sanitization. Unless mitmproxy is used to protect an HTTP/1 service, no action is required. The vulnerability has been fixed in mitmproxy 8.0.0 and above. There are currently no known workarounds.
Severity: HIGH (CVSS v3 base score: 9.8)
Weakness: CWE-444 \u2014 Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling')
The product acts as an intermediary HTTP agent
         (such as a proxy or firewall) in the data flow between two
         entities such as a client and server, but it does not
         interpret malformed HTTP requests or responses in ways that
         are consistent with how the messages will be processed by
         those entities that are at the ultimate destination.
Repository: https://github.com/mitmproxy/mitmproxy
Files changed:
mitmproxy/addons/proxyserver.py
mitmproxy/net/http/http1/__init__.py
mitmproxy/net/http/http1/read.py
mitmproxy/proxy/layers/http/_http1.py
mitmproxy/proxy/layers/http/_http2.py
test/mitmproxy/net/http/http1/test_read.py
test/mitmproxy/proxy/layers/http/test_http.py
test/mitmproxy/proxy/layers/http/test_http2.py
Commit message: security: reject whitespace in HTTP/1 header names

This commit fixes GHSA-gcx2-gvj7-pxv3 by making mitmproxy
reject header names that contain whitespace characters by default.
A new `validate_inbound_headers` option is provided to turn this behavior
off at the expense of allowing HTTP smuggling vulnerabilities.


Vulnerable code:
        if "content-length" in headers:
            raise ValueError("Received both a Transfer-Encoding and a Content-Length header, "
                             "refusing as recommended in RFC 7230 Section 3.3.3. "
                             "See https://github.com/mitmproxy/mitmproxy/issues/4799 for details.")
        validate_inbound_headers=True,
    _read_request_line, _read_response_line, _read_headers, get_header_tokens
    # both content-length and chunked (possible request smuggling)
    with pytest.raises(ValueError, match="Received both a Transfer-Encoding and a Content-Length header"):
        expected_http_body_size(
            treq(headers=Headers(transfer_encoding="chunked", content_length="42")),
        )
@pytest.mark.xfail(reason="inbound validation turned on to protect against request smuggling")

Fixed code:
        loader.add_option(
            "validate_inbound_headers", bool, True,
            """
            Make sure that incoming HTTP requests are not malformed.
            Disabling this option makes mitmproxy vulnerable to HTTP smuggling attacks.
            """,
        )
    validate_headers,
    "validate_headers",
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.2: Header fields are tokens.
# "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /  "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
_valid_header_name = re.compile(rb"^[!#$%&'*+\-.^_`|~0-9a-zA-Z]+$")


def validate_headers(
    headers: Headers
) -> None:
    """
    Validate headers to avoid request smuggling attacks. Raises a ValueError if they are malformed.
    """

    te_found = False
    cl_found = False

    for (name, value) in headers.fields:
        if not _valid_header_name.match(name):
            raise ValueError(f"Received an invalid header name: {name!r}. Invalid header names may introduce "
                             f"request smuggling vulnerabilities. Disable the validate_inbound_headers option "
                             f"to skip this security check.")

        name_lower = name.lower()
        te_found = te_found or name_lower == b"transfer-encoding"
        cl_found = cl_found or name_lower == b"content-length"

    if te_found and cl_found:
        raise ValueError("Received both a Transfer-Encoding and a Content-Length header, "
                         "refusing as recommended in RFC 7230 Section 3.3.3. "
                         "See https://github.com/mitmproxy/mitmproxy/issues/4799 for details. "
                         "Disable the validate_inbound_headers option to skip this security check.")


        # we should make sure that there isn't also a content-length header.
        # this is already handled in validate_headers.
                    if self.context.options.validate_inbound_headers:
                        http1.validate_headers(self.request.headers)
                    if self.context.options.validate_inbound_headers:
                        http1.validate_headers(self.response.headers)
        # validate_inbound_headers is controlled by the validate_inbound_headers option.
        self.h2_conf.validate_inbound_headers = self.context.options.validate_inbound_headers
    _read_request_line, _read_response_line, _read_headers, get_header_tokens, validate_headers
def test_validate_headers():
    # both content-length and chunked (possible request smuggling)
    with pytest.raises(ValueError, match="Received both a Transfer-Encoding and a Content-Length header"):
        validate_headers(
            Headers(transfer_encoding="chunked", content_length="42"),
        )

    with pytest.raises(ValueError, match="Received an invalid header name"):
        validate_headers(
            Headers([(b"content-length ", b"42")]),
        )


def test_request_smuggling_whitespace(tctx):
    """Test that we reject header names with whitespace"""
    err = Placeholder(bytes)
    assert (
        Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False)
        >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\n"
                                     b"Host: example.com\r\n"
                                     b"Content-Length : 42\r\n\r\n")
        << SendData(tctx.client, err)
        << CloseConnection(tctx.client)
    )
    assert b"Received an invalid header name" in err()


def test_request_smuggling_validation_disabled(tctx):
    """Test that we don't reject request smuggling when validation is disabled."""
    tctx.options.validate_inbound_headers = False
    assert (
        Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False)
        >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\n"
                                     b"Host: example.com\r\n"
                                     b"Content-Length: 4\r\n"
                                     b"Transfer-Encoding: chunked\r\n\r\n"
                                     b"4\r\n"
                                     b"abcd\r\n"
                                     b"0\r\n"
                                     b"\r\n")
        << OpenConnection(Placeholder(Server))
    )


    tctx.options.validate_inbound_headers = False