From 821e53438d19facb364fb722c5d07125312a82aa Mon Sep 17 00:00:00 2001 From: Brett Rowan <121075405+b-rowan@users.noreply.github.com> Date: Tue, 5 Mar 2024 12:49:17 -0700 Subject: [PATCH 1/6] Allow ProxyHeadersMiddleware to take a list of hosts or CIDRs. --- uvicorn/middleware/proxy_headers.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/uvicorn/middleware/proxy_headers.py b/uvicorn/middleware/proxy_headers.py index 8f987ab0b..f90672e1a 100644 --- a/uvicorn/middleware/proxy_headers.py +++ b/uvicorn/middleware/proxy_headers.py @@ -10,6 +10,7 @@ """ from __future__ import annotations +import ipaddress from typing import Union, cast from uvicorn._types import ASGI3Application, ASGIReceiveCallable, ASGISendCallable, HTTPScope, Scope, WebSocketScope @@ -23,28 +24,37 @@ def __init__( ) -> None: self.app = app if isinstance(trusted_hosts, str): - self.trusted_hosts = {item.strip() for item in trusted_hosts.split(",")} + trusted_hosts_set = {item.strip() for item in trusted_hosts.split(",")} else: - self.trusted_hosts = set(trusted_hosts) - self.always_trust = "*" in self.trusted_hosts + trusted_hosts_set = set(trusted_hosts) + self.always_trust = "*" in trusted_hosts_set + trusted_hosts_set.discard("*") + + self.trusted_hosts = {ipaddress.ip_network(host) for host in trusted_hosts_set} def get_trusted_client_host(self, x_forwarded_for_hosts: list[str]) -> str | None: if self.always_trust: return x_forwarded_for_hosts[0] for host in reversed(x_forwarded_for_hosts): - if host not in self.trusted_hosts: + if self.check_trusted_host(host): return host return None + def check_trusted_host(self, host: str) -> bool: + for trusted_host in self.trusted_hosts: + if host in trusted_host: + return True + return False + async def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: if scope["type"] in ("http", "websocket"): scope = cast(Union["HTTPScope", "WebSocketScope"], scope) client_addr: tuple[str, int] | None = scope.get("client") client_host = client_addr[0] if client_addr else None - if self.always_trust or client_host in self.trusted_hosts: + if self.always_trust or self.check_trusted_host(client_host): headers = dict(scope["headers"]) if b"x-forwarded-proto" in headers: From f87b7abd04fd9e072ed41e20381f7669a5a2c356 Mon Sep 17 00:00:00 2001 From: Brett Rowan <121075405+b-rowan@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:12:08 -0700 Subject: [PATCH 2/6] Fix bugs with proxy headers. --- uvicorn/middleware/proxy_headers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uvicorn/middleware/proxy_headers.py b/uvicorn/middleware/proxy_headers.py index f90672e1a..fa88a24cc 100644 --- a/uvicorn/middleware/proxy_headers.py +++ b/uvicorn/middleware/proxy_headers.py @@ -37,14 +37,14 @@ def get_trusted_client_host(self, x_forwarded_for_hosts: list[str]) -> str | Non return x_forwarded_for_hosts[0] for host in reversed(x_forwarded_for_hosts): - if self.check_trusted_host(host): + if not self.check_trusted_host(host): return host return None def check_trusted_host(self, host: str) -> bool: - for trusted_host in self.trusted_hosts: - if host in trusted_host: + for trusted_net in self.trusted_hosts: + if ipaddress.ip_address(host) in trusted_net: return True return False From 325441723a70f51d3ba3845adcd4f7945d320b30 Mon Sep 17 00:00:00 2001 From: Brett Rowan <121075405+b-rowan@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:12:35 -0700 Subject: [PATCH 3/6] Add tests for CIDRs. --- tests/middleware/test_proxy_headers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/middleware/test_proxy_headers.py b/tests/middleware/test_proxy_headers.py index 6d7fc8c23..cd624f965 100644 --- a/tests/middleware/test_proxy_headers.py +++ b/tests/middleware/test_proxy_headers.py @@ -43,8 +43,13 @@ async def app( # trusted proxy list (["127.0.0.1", "10.0.0.1"], "Remote: https://1.2.3.4:0"), ("127.0.0.1, 10.0.0.1", "Remote: https://1.2.3.4:0"), + # trusted proxy list with CIDR + (["127.0.0.1", "10.0.0.1", "10.0.1.0/24"], "Remote: https://1.2.3.4:0"), + ("127.0.0.1, 10.0.0.1, 10.0.1.0/24", "Remote: https://1.2.3.4:0"), # request from untrusted proxy ("192.168.0.1", "Remote: http://127.0.0.1:123"), + # request from untrusted proxy with CIDR + ("192.168.0.0/24", "Remote: http://127.0.0.1:123"), ], ) async def test_proxy_headers_trusted_hosts(trusted_hosts: list[str] | str, response_text: str) -> None: @@ -75,6 +80,11 @@ async def test_proxy_headers_trusted_hosts(trusted_hosts: list[str] | str, respo ), # should set first untrusted as remote address (["192.168.0.2", "127.0.0.1"], "Remote: https://10.0.2.1:0"), + # works with CIDRs + ( + ["127.0.0.1", "10.0.2.0/24", "192.168.0.2"], + "Remote: https://1.2.3.4:0", + ), ], ) async def test_proxy_headers_multiple_proxies(trusted_hosts: list[str] | str, response_text: str) -> None: From b6b444750f7ec42ff00b35aff0dbd523ccd00524 Mon Sep 17 00:00:00 2001 From: Brett Rowan <121075405+b-rowan@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:20:12 -0700 Subject: [PATCH 4/6] Fix linter complaints (wasn't even my type hinting?) --- uvicorn/middleware/proxy_headers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/uvicorn/middleware/proxy_headers.py b/uvicorn/middleware/proxy_headers.py index fa88a24cc..13b536bcf 100644 --- a/uvicorn/middleware/proxy_headers.py +++ b/uvicorn/middleware/proxy_headers.py @@ -32,7 +32,7 @@ def __init__( self.trusted_hosts = {ipaddress.ip_network(host) for host in trusted_hosts_set} - def get_trusted_client_host(self, x_forwarded_for_hosts: list[str]) -> str | None: + def get_trusted_client_host(self, x_forwarded_for_hosts: list[str]) -> str: if self.always_trust: return x_forwarded_for_hosts[0] @@ -40,8 +40,6 @@ def get_trusted_client_host(self, x_forwarded_for_hosts: list[str]) -> str | Non if not self.check_trusted_host(host): return host - return None - def check_trusted_host(self, host: str) -> bool: for trusted_net in self.trusted_hosts: if ipaddress.ip_address(host) in trusted_net: From 5efc89fb0474881f8653fa7afa2c518d929363ef Mon Sep 17 00:00:00 2001 From: Brett Rowan <121075405+b-rowan@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:24:29 -0700 Subject: [PATCH 5/6] Return empty `str` to satisfy linter --- uvicorn/middleware/proxy_headers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/uvicorn/middleware/proxy_headers.py b/uvicorn/middleware/proxy_headers.py index 13b536bcf..3d3c0e36c 100644 --- a/uvicorn/middleware/proxy_headers.py +++ b/uvicorn/middleware/proxy_headers.py @@ -40,6 +40,9 @@ def get_trusted_client_host(self, x_forwarded_for_hosts: list[str]) -> str: if not self.check_trusted_host(host): return host + return "" + + def check_trusted_host(self, host: str) -> bool: for trusted_net in self.trusted_hosts: if ipaddress.ip_address(host) in trusted_net: @@ -72,6 +75,6 @@ async def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGIS x_forwarded_for_hosts = [item.strip() for item in x_forwarded_for.split(",")] host = self.get_trusted_client_host(x_forwarded_for_hosts) port = 0 - scope["client"] = (host, port) # type: ignore[arg-type] + scope["client"] = (host, port) return await self.app(scope, receive, send) From b633b05b76f0a21555c98b2009f3d50713e55b17 Mon Sep 17 00:00:00 2001 From: Brett Rowan <121075405+b-rowan@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:38:36 -0700 Subject: [PATCH 6/6] Remove extra line to satisfy the linter --- uvicorn/middleware/proxy_headers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/uvicorn/middleware/proxy_headers.py b/uvicorn/middleware/proxy_headers.py index 3d3c0e36c..32b45290b 100644 --- a/uvicorn/middleware/proxy_headers.py +++ b/uvicorn/middleware/proxy_headers.py @@ -41,7 +41,6 @@ def get_trusted_client_host(self, x_forwarded_for_hosts: list[str]) -> str: return host return "" - def check_trusted_host(self, host: str) -> bool: for trusted_net in self.trusted_hosts: