From 9e6202279afe3e34b0d7f11d483a9b48665f8bc8 Mon Sep 17 00:00:00 2001 From: Mohil-Ahuja Date: Mon, 9 Nov 2026 15:02:37 +0530 Subject: [PATCH] fix(gql): Remove credentials and query values from the transport URL The integration copied transport.url into event["request"]["url"] unchanged, so credentials and query tokens in the URL were sent with every event, whatever send_default_pii was set to. Sanitize it with parse_url and send the filtered query in query_string. --- sentry_sdk/integrations/gql.py | 13 +++++- tests/integrations/gql/test_gql.py | 65 +++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/gql.py b/sentry_sdk/integrations/gql.py index 37759b72d9..a057714c35 100644 --- a/sentry_sdk/integrations/gql.py +++ b/sentry_sdk/integrations/gql.py @@ -2,9 +2,11 @@ from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version from sentry_sdk.scope import should_send_default_pii from sentry_sdk.utils import ( + capture_internal_exceptions, ensure_integration_enabled, event_from_exception, has_data_collection_enabled, + parse_url, parse_version, ) @@ -99,9 +101,16 @@ def _request_info_from_transport( } try: - request_info["url"] = transport.url + url = transport.url except AttributeError: - pass + return request_info + + with capture_internal_exceptions(): + # Some transports accept a URL object as well as a string. + parsed_url = parse_url(str(url)) + request_info["url"] = parsed_url.url + if parsed_url.query: + request_info["query_string"] = parsed_url.query return request_info diff --git a/tests/integrations/gql/test_gql.py b/tests/integrations/gql/test_gql.py index dcaee5be10..464aaa68fd 100644 --- a/tests/integrations/gql/test_gql.py +++ b/tests/integrations/gql/test_gql.py @@ -1,10 +1,12 @@ +import re + import pytest import responses from gql import Client, __version__, gql from gql.transport.exceptions import TransportQueryError from gql.transport.requests import RequestsHTTPTransport -from sentry_sdk.integrations.gql import GQLIntegration +from sentry_sdk.integrations.gql import GQLIntegration, _request_info_from_transport from sentry_sdk.utils import parse_version GQL_VERSION = parse_version(__version__) @@ -273,3 +275,64 @@ def test_real_gql_request_with_error_data_collection_variables( assert event["request"]["data"].get("variables") else: assert not event["request"]["data"].get("variables") + + +@pytest.mark.parametrize("send_default_pii", (False, True)) +@responses.activate +def test_transport_url_is_sanitized(sentry_init, capture_events, send_default_pii): + sentry_init(integrations=[GQLIntegration()], send_default_pii=send_default_pii) + events = capture_events() + + responses.add( + method=responses.POST, + url=re.compile(r"https://.*api\.example\.com/graphql.*"), + json={"errors": [{"message": "some error"}]}, + status=200, + ) + transport = RequestsHTTPTransport( + url="https://user:password@api.example.com/graphql?access_token=secret&v=1#frag" + ) + client = Client(transport=transport) + + with pytest.raises(TransportQueryError): + client.execute(gql("query Example { example }")) + + (event,) = events + assert event["request"]["url"] == ( + "https://[Filtered]:[Filtered]@api.example.com/graphql" + ) + assert event["request"]["query_string"] == "access_token=[Filtered]&v=[Filtered]" + assert "password" not in str(event["request"]) + assert "secret" not in str(event["request"]) + + +def test_transport_url_without_query_has_no_query_string(): + class FakeTransport: + url = "https://api.example.com/graphql" + + assert _request_info_from_transport(FakeTransport()) == { + "method": "POST", + "url": "https://api.example.com/graphql", + } + + +def test_transport_url_object_is_sanitized(): + class FakeUrl: + def __str__(self): + return "https://user:password@api.example.com/graphql?token=secret" + + class FakeTransport: + url = FakeUrl() + + assert _request_info_from_transport(FakeTransport()) == { + "method": "POST", + "url": "https://[Filtered]:[Filtered]@api.example.com/graphql", + "query_string": "token=[Filtered]", + } + + +def test_transport_without_url(): + class FakeTransport: + pass + + assert _request_info_from_transport(FakeTransport()) == {"method": "POST"}