Skip to content

Commit 3a0bd74

Browse files
ahmedetefysentry-bot
andauthored
fix(django): Added SDK logic that honors the X-Forwarded-For header (getsentry#1037)
* Passed django setting USE_X_FORWARDED_FOR to sentry wsgi middleware upon creation * Linting changes * Accessed settings attr correctly * Added django tests for django setting of USE_X_FORWARDED_HOST and extracting the correct request url from it * fix: Formatting Co-authored-by: sentry-bot <markus+ghbot@sentry.io>
1 parent ed7d722 commit 3a0bd74

File tree

3 files changed

+73
-14
lines changed

3 files changed

+73
-14
lines changed

sentry_sdk/integrations/django/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,13 @@ def sentry_patched_wsgi_handler(self, environ, start_response):
120120

121121
bound_old_app = old_app.__get__(self, WSGIHandler)
122122

123-
return SentryWsgiMiddleware(bound_old_app)(environ, start_response)
123+
from django.conf import settings
124+
125+
use_x_forwarded_for = settings.USE_X_FORWARDED_HOST
126+
127+
return SentryWsgiMiddleware(bound_old_app, use_x_forwarded_for)(
128+
environ, start_response
129+
)
124130

125131
WSGIHandler.__call__ = sentry_patched_wsgi_handler
126132

sentry_sdk/integrations/wsgi.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,16 @@ def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
5454
return s.encode("latin1").decode(charset, errors)
5555

5656

57-
def get_host(environ):
58-
# type: (Dict[str, str]) -> str
57+
def get_host(environ, use_x_forwarded_for=False):
58+
# type: (Dict[str, str], bool) -> str
5959
"""Return the host for the given WSGI environment. Yanked from Werkzeug."""
60-
if environ.get("HTTP_HOST"):
60+
if use_x_forwarded_for and "HTTP_X_FORWARDED_HOST" in environ:
61+
rv = environ["HTTP_X_FORWARDED_HOST"]
62+
if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"):
63+
rv = rv[:-3]
64+
elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"):
65+
rv = rv[:-4]
66+
elif environ.get("HTTP_HOST"):
6167
rv = environ["HTTP_HOST"]
6268
if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"):
6369
rv = rv[:-3]
@@ -77,23 +83,24 @@ def get_host(environ):
7783
return rv
7884

7985

80-
def get_request_url(environ):
81-
# type: (Dict[str, str]) -> str
86+
def get_request_url(environ, use_x_forwarded_for=False):
87+
# type: (Dict[str, str], bool) -> str
8288
"""Return the absolute URL without query string for the given WSGI
8389
environment."""
8490
return "%s://%s/%s" % (
8591
environ.get("wsgi.url_scheme"),
86-
get_host(environ),
92+
get_host(environ, use_x_forwarded_for),
8793
wsgi_decoding_dance(environ.get("PATH_INFO") or "").lstrip("/"),
8894
)
8995

9096

9197
class SentryWsgiMiddleware(object):
92-
__slots__ = ("app",)
98+
__slots__ = ("app", "use_x_forwarded_for")
9399

94-
def __init__(self, app):
95-
# type: (Callable[[Dict[str, str], Callable[..., Any]], Any]) -> None
100+
def __init__(self, app, use_x_forwarded_for=False):
101+
# type: (Callable[[Dict[str, str], Callable[..., Any]], Any], bool) -> None
96102
self.app = app
103+
self.use_x_forwarded_for = use_x_forwarded_for
97104

98105
def __call__(self, environ, start_response):
99106
# type: (Dict[str, str], Callable[..., Any]) -> _ScopedResponse
@@ -110,7 +117,9 @@ def __call__(self, environ, start_response):
110117
scope.clear_breadcrumbs()
111118
scope._name = "wsgi"
112119
scope.add_event_processor(
113-
_make_wsgi_event_processor(environ)
120+
_make_wsgi_event_processor(
121+
environ, self.use_x_forwarded_for
122+
)
114123
)
115124

116125
transaction = Transaction.continue_from_environ(
@@ -269,8 +278,8 @@ def close(self):
269278
reraise(*_capture_exception(self._hub))
270279

271280

272-
def _make_wsgi_event_processor(environ):
273-
# type: (Dict[str, str]) -> EventProcessor
281+
def _make_wsgi_event_processor(environ, use_x_forwarded_for):
282+
# type: (Dict[str, str], bool) -> EventProcessor
274283
# It's a bit unfortunate that we have to extract and parse the request data
275284
# from the environ so eagerly, but there are a few good reasons for this.
276285
#
@@ -284,7 +293,7 @@ def _make_wsgi_event_processor(environ):
284293
# https://github.com/unbit/uwsgi/issues/1950
285294

286295
client_ip = get_client_ip(environ)
287-
request_url = get_request_url(environ)
296+
request_url = get_request_url(environ, use_x_forwarded_for)
288297
query_string = environ.get("QUERY_STRING")
289298
method = environ.get("REQUEST_METHOD")
290299
env = dict(_get_environ(environ))

tests/integrations/django/test_basic.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,50 @@ def test_view_exceptions(sentry_init, client, capture_exceptions, capture_events
4040
assert event["exception"]["values"][0]["mechanism"]["type"] == "django"
4141

4242

43+
def test_ensures_x_forwarded_header_is_honored_in_sdk_when_enabled_in_django(
44+
sentry_init, client, capture_exceptions, capture_events
45+
):
46+
"""
47+
Test that ensures if django settings.USE_X_FORWARDED_HOST is set to True
48+
then the SDK sets the request url to the `HTTP_X_FORWARDED_FOR`
49+
"""
50+
from django.conf import settings
51+
52+
settings.USE_X_FORWARDED_HOST = True
53+
54+
sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
55+
exceptions = capture_exceptions()
56+
events = capture_events()
57+
client.get(reverse("view_exc"), headers={"X_FORWARDED_HOST": "example.com"})
58+
59+
(error,) = exceptions
60+
assert isinstance(error, ZeroDivisionError)
61+
62+
(event,) = events
63+
assert event["request"]["url"] == "http://example.com/view-exc"
64+
65+
settings.USE_X_FORWARDED_HOST = False
66+
67+
68+
def test_ensures_x_forwarded_header_is_not_honored_when_unenabled_in_django(
69+
sentry_init, client, capture_exceptions, capture_events
70+
):
71+
"""
72+
Test that ensures if django settings.USE_X_FORWARDED_HOST is set to False
73+
then the SDK sets the request url to the `HTTP_POST`
74+
"""
75+
sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
76+
exceptions = capture_exceptions()
77+
events = capture_events()
78+
client.get(reverse("view_exc"), headers={"X_FORWARDED_HOST": "example.com"})
79+
80+
(error,) = exceptions
81+
assert isinstance(error, ZeroDivisionError)
82+
83+
(event,) = events
84+
assert event["request"]["url"] == "http://localhost/view-exc"
85+
86+
4387
def test_middleware_exceptions(sentry_init, client, capture_exceptions):
4488
sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
4589
exceptions = capture_exceptions()

0 commit comments

Comments
 (0)