From 46f3672b8ed02da869c18ee0f7f75d7c7a6b5ed2 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 30 Sep 2026 09:47:48 +0200 Subject: [PATCH 1/5] feat(wsgi): Add sentry.is_localhost for inbound filters --- sentry_sdk/integrations/wsgi.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index ab04308f6e..f541b0aae6 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -131,9 +131,7 @@ def __call__( SPANDATA.SENTRY_IS_LOCALHOST, _is_localhost( client_ip=get_client_ip(environ), - url_host=environ.get("SERVER_NAME") - if not environ.get("HTTP_HOST") - else None, + url_host=environ.get("SERVER_NAME"), host_header=environ.get("HTTP_HOST"), forwarded_host_header=environ.get("HTTP_X_FORWARDED_HOST"), ), From 1903026f205af4d73146c9395430c794404c5120 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 30 Sep 2026 10:04:12 +0200 Subject: [PATCH 2/5] feat(asgi): Add `sentry.is_localhost` for inbound filters --- sentry_sdk/integrations/asgi.py | 13 ++++++ sentry_sdk/integrations/wsgi.py | 4 +- tests/integrations/asgi/test_asgi.py | 70 ++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 17ffbaacf7..12d100260d 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -48,6 +48,7 @@ capture_internal_exceptions, event_from_exception, has_data_collection_enabled, + is_localhost, logger, nullcontext, qualname_from_function, @@ -232,6 +233,18 @@ async def _run_app( processor = partial(self.event_processor, asgi_scope=scope) sentry_scope.add_event_processor(processor) + headers = _get_headers(scope) + server = scope.get("server") + sentry_scope.set_attribute( + SPANDATA.SENTRY_IS_LOCALHOST, + is_localhost( + client_ip=_get_ip(scope) if scope.get("client") else None, + url_host=server[0] if server else None, + host_header=headers.get("host"), + forwarded_host_header=headers.get("x-forwarded-host"), + ), + ) + ty = scope["type"] ( transaction_name, diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index f541b0aae6..ab04308f6e 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -131,7 +131,9 @@ def __call__( SPANDATA.SENTRY_IS_LOCALHOST, _is_localhost( client_ip=get_client_ip(environ), - url_host=environ.get("SERVER_NAME"), + url_host=environ.get("SERVER_NAME") + if not environ.get("HTTP_HOST") + else None, host_header=environ.get("HTTP_HOST"), forwarded_host_header=environ.get("HTTP_X_FORWARDED_HOST"), ), diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index 2de17ab15a..502f5b2bb4 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -1490,3 +1490,73 @@ async def wrapped_app(scope, receive, send): else: assert "user.ip_address" not in server_span["attributes"] assert "user.ip_address" not in child_span["attributes"] + + +@pytest.mark.parametrize( + "client_ip, server, host_header, is_localhost", + [ + # Loopback IP + ("127.0.0.1", ("example.com", 80), b"example.com", True), + # IPv6 loopback + ("::1", ("example.com", 80), b"example.com", True), + # Localhost host header with non-local IP + ("203.0.113.50", ("example.com", 80), b"localhost:8000", True), + # Server bound to localhost + ("203.0.113.50", ("localhost", 8000), b"example.com", True), + # Non-local everything + ("203.0.113.50", ("example.com", 80), b"example.com", False), + ], +) +@pytest.mark.asyncio +async def test_is_localhost_attribute( + sentry_init, + capture_items, + client_ip, + server, + host_header, + is_localhost, +): + async def app(scope, receive, send): + if scope["type"] == "lifespan": + while True: + message = await receive() + if message["type"] == "lifespan.startup": + await send({"type": "lifespan.startup.complete"}) + elif message["type"] == "lifespan.shutdown": + await send({"type": "lifespan.shutdown.complete"}) + return + + with sentry_sdk.traces.start_span(name="child-span"): + pass + + await send( + { + "type": "http.response.start", + "status": 200, + "headers": [[b"content-type", b"text/plain"]], + } + ) + await send({"type": "http.response.body", "body": b"Hello, world!"}) + + sentry_init(traces_sample_rate=1.0, trace_lifecycle="stream") + sentry_app = SentryAsgiMiddleware(app) + + async def wrapped_app(scope, receive, send): + if scope["type"] != "lifespan": + scope["client"] = (client_ip, 0) + scope["server"] = server + scope["headers"] = [(k, v) for k, v in scope["headers"] if k != b"host"] + scope["headers"].append((b"host", host_header)) + + await sentry_app(scope, receive, send) + + async with TestClient(wrapped_app) as client: + items = capture_items("span") + await client.get("/some_url") + + sentry_sdk.flush() + + child_span, server_span = [item.payload for item in items] + + assert server_span["attributes"]["sentry.is_localhost"] is is_localhost + assert child_span["attributes"]["sentry.is_localhost"] is is_localhost From 053e1ab58899de2a324b81b02f1e6cfaa8aa5073 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 30 Sep 2026 10:54:34 +0200 Subject: [PATCH 3/5] fixes --- sentry_sdk/integrations/asgi.py | 9 +++++---- tests/integrations/asgi/test_asgi.py | 9 ++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 12d100260d..7918dd29f9 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -45,10 +45,10 @@ HAS_REAL_CONTEXTVARS, ContextVar, _get_installed_modules, + _is_localhost, capture_internal_exceptions, event_from_exception, has_data_collection_enabled, - is_localhost, logger, nullcontext, qualname_from_function, @@ -234,13 +234,14 @@ async def _run_app( sentry_scope.add_event_processor(processor) headers = _get_headers(scope) + host_header = headers.get("host") server = scope.get("server") sentry_scope.set_attribute( SPANDATA.SENTRY_IS_LOCALHOST, - is_localhost( + _is_localhost( client_ip=_get_ip(scope) if scope.get("client") else None, - url_host=server[0] if server else None, - host_header=headers.get("host"), + url_host=server[0] if server and not host_header else None, + host_header=host_header, forwarded_host_header=headers.get("x-forwarded-host"), ), ) diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index 502f5b2bb4..d4d47f7831 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -1501,8 +1501,10 @@ async def wrapped_app(scope, receive, send): ("::1", ("example.com", 80), b"example.com", True), # Localhost host header with non-local IP ("203.0.113.50", ("example.com", 80), b"localhost:8000", True), - # Server bound to localhost - ("203.0.113.50", ("localhost", 8000), b"example.com", True), + # Server bound to localhost, but host header is public (reverse proxy) + ("203.0.113.50", ("localhost", 8000), b"example.com", False), + # Server bound to localhost, no host header (fallback to server) + ("203.0.113.50", ("localhost", 8000), None, True), # Non-local everything ("203.0.113.50", ("example.com", 80), b"example.com", False), ], @@ -1546,7 +1548,8 @@ async def wrapped_app(scope, receive, send): scope["client"] = (client_ip, 0) scope["server"] = server scope["headers"] = [(k, v) for k, v in scope["headers"] if k != b"host"] - scope["headers"].append((b"host", host_header)) + if host_header is not None: + scope["headers"].append((b"host", host_header)) await sentry_app(scope, receive, send) From 121277b7ac6ac7cb602a00af0ff687c94836d81d Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 30 Sep 2026 10:55:40 +0200 Subject: [PATCH 4/5] simplify --- sentry_sdk/integrations/asgi.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 7918dd29f9..a4025a02b0 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -234,14 +234,14 @@ async def _run_app( sentry_scope.add_event_processor(processor) headers = _get_headers(scope) - host_header = headers.get("host") - server = scope.get("server") sentry_scope.set_attribute( SPANDATA.SENTRY_IS_LOCALHOST, _is_localhost( client_ip=_get_ip(scope) if scope.get("client") else None, - url_host=server[0] if server and not host_header else None, - host_header=host_header, + url_host=scope.get("server")[0] + if scope.get("server") and not headers.get("host") + else None, + host_header=headers.get("host"), forwarded_host_header=headers.get("x-forwarded-host"), ), ) From adf9025b3115f1ef149465ba22eae349d3b6a610 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 30 Sep 2026 11:26:46 +0200 Subject: [PATCH 5/5] . --- sentry_sdk/integrations/_asgi_common.py | 7 +++++-- sentry_sdk/integrations/asgi.py | 11 +++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/integrations/_asgi_common.py b/sentry_sdk/integrations/_asgi_common.py index dbf02decdc..d77d9a084f 100644 --- a/sentry_sdk/integrations/_asgi_common.py +++ b/sentry_sdk/integrations/_asgi_common.py @@ -88,7 +88,7 @@ def _get_query(asgi_scope: "Any") -> "Optional[str]": return urllib.parse.unquote(qs.decode("latin-1")) -def _get_ip(asgi_scope: "Any") -> str: +def _get_ip(asgi_scope: "Any") -> "Optional[str]": """ Extract IP Address from the ASGI scope based on request headers with fallback to scope client. """ @@ -103,7 +103,10 @@ def _get_ip(asgi_scope: "Any") -> str: except KeyError: pass - return asgi_scope.get("client")[0] + if asgi_scope.get("client"): + return asgi_scope["client"][0] + + return None def _get_request_data( diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index a4025a02b0..073b1ee376 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -237,7 +237,7 @@ async def _run_app( sentry_scope.set_attribute( SPANDATA.SENTRY_IS_LOCALHOST, _is_localhost( - client_ip=_get_ip(scope) if scope.get("client") else None, + client_ip=_get_ip(scope), url_host=scope.get("server")[0] if scope.get("server") and not headers.get("host") else None, @@ -268,17 +268,16 @@ async def _run_app( "network.protocol.name": ty, } - if scope.get("client"): + ip = _get_ip(scope) + if ip: client_options = sentry_sdk.get_client().options if has_data_collection_enabled(client_options): if client_options["data_collection"]["user_info"]: sentry_scope.set_attribute( - SPANDATA.USER_IP_ADDRESS, _get_ip(scope) + SPANDATA.USER_IP_ADDRESS, ip ) elif should_send_default_pii(): - sentry_scope.set_attribute( - SPANDATA.USER_IP_ADDRESS, _get_ip(scope) - ) + sentry_scope.set_attribute(SPANDATA.USER_IP_ADDRESS, ip) if ty in ("http", "websocket"): if (