diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 2b571f5e11..40f6ab3011 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -120,7 +120,13 @@ def sentry_patched_wsgi_handler(self, environ, start_response): bound_old_app = old_app.__get__(self, WSGIHandler) - return SentryWsgiMiddleware(bound_old_app)(environ, start_response) + from django.conf import settings + + use_x_forwarded_for = settings.USE_X_FORWARDED_HOST + + return SentryWsgiMiddleware(bound_old_app, use_x_forwarded_for)( + environ, start_response + ) WSGIHandler.__call__ = sentry_patched_wsgi_handler diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 2f63298ffa..4f274fa00c 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -54,10 +54,16 @@ def wsgi_decoding_dance(s, charset="utf-8", errors="replace"): return s.encode("latin1").decode(charset, errors) -def get_host(environ): - # type: (Dict[str, str]) -> str +def get_host(environ, use_x_forwarded_for=False): + # type: (Dict[str, str], bool) -> str """Return the host for the given WSGI environment. Yanked from Werkzeug.""" - if environ.get("HTTP_HOST"): + if use_x_forwarded_for and "HTTP_X_FORWARDED_HOST" in environ: + rv = environ["HTTP_X_FORWARDED_HOST"] + if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"): + rv = rv[:-3] + elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"): + rv = rv[:-4] + elif environ.get("HTTP_HOST"): rv = environ["HTTP_HOST"] if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"): rv = rv[:-3] @@ -77,23 +83,24 @@ def get_host(environ): return rv -def get_request_url(environ): - # type: (Dict[str, str]) -> str +def get_request_url(environ, use_x_forwarded_for=False): + # type: (Dict[str, str], bool) -> str """Return the absolute URL without query string for the given WSGI environment.""" return "%s://%s/%s" % ( environ.get("wsgi.url_scheme"), - get_host(environ), + get_host(environ, use_x_forwarded_for), wsgi_decoding_dance(environ.get("PATH_INFO") or "").lstrip("/"), ) class SentryWsgiMiddleware(object): - __slots__ = ("app",) + __slots__ = ("app", "use_x_forwarded_for") - def __init__(self, app): - # type: (Callable[[Dict[str, str], Callable[..., Any]], Any]) -> None + def __init__(self, app, use_x_forwarded_for=False): + # type: (Callable[[Dict[str, str], Callable[..., Any]], Any], bool) -> None self.app = app + self.use_x_forwarded_for = use_x_forwarded_for def __call__(self, environ, start_response): # type: (Dict[str, str], Callable[..., Any]) -> _ScopedResponse @@ -110,7 +117,9 @@ def __call__(self, environ, start_response): scope.clear_breadcrumbs() scope._name = "wsgi" scope.add_event_processor( - _make_wsgi_event_processor(environ) + _make_wsgi_event_processor( + environ, self.use_x_forwarded_for + ) ) transaction = Transaction.continue_from_environ( @@ -269,8 +278,8 @@ def close(self): reraise(*_capture_exception(self._hub)) -def _make_wsgi_event_processor(environ): - # type: (Dict[str, str]) -> EventProcessor +def _make_wsgi_event_processor(environ, use_x_forwarded_for): + # type: (Dict[str, str], bool) -> EventProcessor # It's a bit unfortunate that we have to extract and parse the request data # from the environ so eagerly, but there are a few good reasons for this. # @@ -284,7 +293,7 @@ def _make_wsgi_event_processor(environ): # https://github.com/unbit/uwsgi/issues/1950 client_ip = get_client_ip(environ) - request_url = get_request_url(environ) + request_url = get_request_url(environ, use_x_forwarded_for) query_string = environ.get("QUERY_STRING") method = environ.get("REQUEST_METHOD") env = dict(_get_environ(environ)) diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py index e094d23a72..5a4d801374 100644 --- a/tests/integrations/django/test_basic.py +++ b/tests/integrations/django/test_basic.py @@ -40,6 +40,50 @@ def test_view_exceptions(sentry_init, client, capture_exceptions, capture_events assert event["exception"]["values"][0]["mechanism"]["type"] == "django" +def test_ensures_x_forwarded_header_is_honored_in_sdk_when_enabled_in_django( + sentry_init, client, capture_exceptions, capture_events +): + """ + Test that ensures if django settings.USE_X_FORWARDED_HOST is set to True + then the SDK sets the request url to the `HTTP_X_FORWARDED_FOR` + """ + from django.conf import settings + + settings.USE_X_FORWARDED_HOST = True + + sentry_init(integrations=[DjangoIntegration()], send_default_pii=True) + exceptions = capture_exceptions() + events = capture_events() + client.get(reverse("view_exc"), headers={"X_FORWARDED_HOST": "example.com"}) + + (error,) = exceptions + assert isinstance(error, ZeroDivisionError) + + (event,) = events + assert event["request"]["url"] == "http://example.com/view-exc" + + settings.USE_X_FORWARDED_HOST = False + + +def test_ensures_x_forwarded_header_is_not_honored_when_unenabled_in_django( + sentry_init, client, capture_exceptions, capture_events +): + """ + Test that ensures if django settings.USE_X_FORWARDED_HOST is set to False + then the SDK sets the request url to the `HTTP_POST` + """ + sentry_init(integrations=[DjangoIntegration()], send_default_pii=True) + exceptions = capture_exceptions() + events = capture_events() + client.get(reverse("view_exc"), headers={"X_FORWARDED_HOST": "example.com"}) + + (error,) = exceptions + assert isinstance(error, ZeroDivisionError) + + (event,) = events + assert event["request"]["url"] == "http://localhost/view-exc" + + def test_middleware_exceptions(sentry_init, client, capture_exceptions): sentry_init(integrations=[DjangoIntegration()], send_default_pii=True) exceptions = capture_exceptions()