Insufficient Protection against HTTP Request Smuggling in mitmproxy
Description
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.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
mitmproxyPyPI | < 8.0.0 | 8.0.0 |
Affected products
1Patches
1b06fb6d15708security: reject whitespace in HTTP/1 header names
8 files changed · +95 −12
mitmproxy/addons/proxyserver.py+7 −0 modified@@ -98,6 +98,13 @@ def load(self, loader): in custom scripts are lowercased before they are sent. """, ) + 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. + """, + ) async def running(self): self.master = ctx.master
mitmproxy/net/http/http1/__init__.py+2 −0 modified@@ -3,6 +3,7 @@ read_response_head, connection_close, expected_http_body_size, + validate_headers, ) from .assemble import ( assemble_request, assemble_request_head, @@ -16,6 +17,7 @@ "read_response_head", "connection_close", "expected_http_body_size", + "validate_headers", "assemble_request", "assemble_request_head", "assemble_response", "assemble_response_head", "assemble_body",
mitmproxy/net/http/http1/read.py+34 −4 modified@@ -38,6 +38,38 @@ def connection_close(http_version, 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.") + + def expected_http_body_size( request: Request, response: Optional[Response] = None @@ -101,10 +133,8 @@ def expected_http_body_size( # a message downstream. # if "transfer-encoding" in headers: - 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.") + # we should make sure that there isn't also a content-length header. + # this is already handled in validate_headers. te: str = headers["transfer-encoding"] if not te.isascii():
mitmproxy/proxy/layers/http/_http1.py+4 −0 modified@@ -234,6 +234,8 @@ def read_headers(self, event: events.ConnectionEvent) -> layer.CommandGenerator[ request_head = [bytes(x) for x in request_head] # TODO: Make url.parse compatible with bytearrays try: self.request = http1.read_request_head(request_head) + if self.context.options.validate_inbound_headers: + http1.validate_headers(self.request.headers) expected_body_size = http1.expected_http_body_size(self.request) except ValueError as e: yield commands.SendData(self.conn, make_error_response(400, str(e))) @@ -330,6 +332,8 @@ def read_headers(self, event: events.ConnectionEvent) -> layer.CommandGenerator[ response_head = [bytes(x) for x in response_head] # TODO: Make url.parse compatible with bytearrays try: self.response = http1.read_response_head(response_head) + if self.context.options.validate_inbound_headers: + http1.validate_headers(self.response.headers) expected_size = http1.expected_http_body_size(self.request, self.response) except ValueError as e: yield commands.CloseConnection(self.conn)
mitmproxy/proxy/layers/http/_http2.py+2 −1 modified@@ -40,7 +40,7 @@ class Http2Connection(HttpConnection): h2_conf_defaults = dict( header_encoding=False, validate_outbound_headers=False, - validate_inbound_headers=True, + # validate_inbound_headers is controlled by the validate_inbound_headers option. normalize_inbound_headers=False, # changing this to True is required to pass h2spec normalize_outbound_headers=False, ) @@ -58,6 +58,7 @@ def __init__(self, context: Context, conn: Connection): if self.debug: self.h2_conf.logger = H2ConnectionLogger(f"{human.format_address(self.context.client.peername)}: " f"{self.__class__.__name__}") + self.h2_conf.validate_inbound_headers = self.context.options.validate_inbound_headers self.h2_conn = BufferedH2Connection(self.h2_conf) self.streams = {}
test/mitmproxy/net/http/http1/test_read.py+14 −6 modified@@ -4,7 +4,7 @@ from mitmproxy.net.http.http1.read import ( read_request_head, read_response_head, connection_close, expected_http_body_size, - _read_request_line, _read_response_line, _read_headers, get_header_tokens + _read_request_line, _read_response_line, _read_headers, get_header_tokens, validate_headers ) from mitmproxy.test.tutils import treq, tresp @@ -59,6 +59,19 @@ def test_read_response_head(): assert r.content is None +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_expected_http_body_size(): # Expect: 100-continue assert expected_http_body_size( @@ -91,11 +104,6 @@ def test_expected_http_body_size(): assert expected_http_body_size( treq(headers=Headers(transfer_encoding="gzip,\tchunked")), ) is None - # 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")), - ) with pytest.raises(ValueError, match="Invalid transfer encoding"): expected_http_body_size( treq(headers=Headers(transfer_encoding="chun\u212Aed")), # "chunKed".lower() == "chunked"
test/mitmproxy/proxy/layers/http/test_http2.py+1 −1 modified@@ -352,11 +352,11 @@ def enable_response_streaming(flow: HTTPFlow): assert "peer closed connection" in flow().error.msg -@pytest.mark.xfail(reason="inbound validation turned on to protect against request smuggling") @pytest.mark.parametrize("normalize", [True, False]) def test_no_normalization(tctx, normalize): """Test that we don't normalize headers when we just pass them through.""" tctx.options.normalize_outbound_headers = normalize + tctx.options.validate_inbound_headers = False server = Placeholder(Server) flow = Placeholder(HTTPFlow)
test/mitmproxy/proxy/layers/http/test_http.py+31 −0 modified@@ -1261,6 +1261,37 @@ def test_request_smuggling(tctx): assert b"Received both a Transfer-Encoding and a Content-Length header" in err() +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)) + ) + + def test_request_smuggling_te_te(tctx): """Test that we reject transfer-encoding headers that are weird in some way""" err = Placeholder(bytes)
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-gcx2-gvj7-pxv3ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-24766ghsaADVISORY
- github.com/mitmproxy/mitmproxy/commit/b06fb6d157087d526bd02e7aadbe37c56865c71bghsax_refsource_MISCWEB
- github.com/mitmproxy/mitmproxy/security/advisories/GHSA-gcx2-gvj7-pxv3ghsax_refsource_CONFIRMWEB
- github.com/pypa/advisory-database/tree/main/vulns/mitmproxy/PYSEC-2022-170.yamlghsaWEB
- mitmproxy.org/posts/releases/mitmproxy8ghsaWEB
- mitmproxy.org/posts/releases/mitmproxy8/mitrex_refsource_MISC
News mentions
0No linked articles in our index yet.