diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index db50e058f4..a5fe541dc2 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -44,6 +44,8 @@ DEFAULT_QUEUE_SIZE = 100 DEFAULT_MAX_BREADCRUMBS = 100 +SENSITIVE_DATA_SUBSTITUTE = "[Filtered]" + class INSTRUMENTER: SENTRY = "sentry" diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 67a0bf3844..697ab484e3 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -6,13 +6,14 @@ import weakref from sentry_sdk._types import MYPY -from sentry_sdk.consts import OP +from sentry_sdk.consts import OP, SENSITIVE_DATA_SUBSTITUTE from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.scope import add_global_event_processor from sentry_sdk.serializer import add_global_repr_processor from sentry_sdk.tracing import SOURCE_FOR_STYLE, TRANSACTION_SOURCE_URL from sentry_sdk.tracing_utils import record_sql_queries from sentry_sdk.utils import ( + AnnotatedValue, HAS_REAL_CONTEXTVARS, CONTEXTVARS_ERROR_MESSAGE, logger, @@ -28,6 +29,7 @@ try: from django import VERSION as DJANGO_VERSION + from django.conf import settings as django_settings from django.core import signals try: @@ -476,8 +478,20 @@ def env(self): return self.request.META def cookies(self): - # type: () -> Dict[str, str] - return self.request.COOKIES + # type: () -> Dict[str, Union[str, AnnotatedValue]] + privacy_cookies = [ + django_settings.CSRF_COOKIE_NAME, + django_settings.SESSION_COOKIE_NAME, + ] + + clean_cookies = {} # type: Dict[str, Union[str, AnnotatedValue]] + for (key, val) in self.request.COOKIES.items(): + if key in privacy_cookies: + clean_cookies[key] = SENSITIVE_DATA_SUBSTITUTE + else: + clean_cookies[key] = val + + return clean_cookies def raw_data(self): # type: () -> bytes diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 4d6a091398..3f573171a6 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -370,6 +370,24 @@ def removed_because_over_size_limit(cls): }, ) + @classmethod + def substituted_because_contains_sensitive_data(cls): + # type: () -> AnnotatedValue + """The actual value was removed because it contained sensitive information.""" + from sentry_sdk.consts import SENSITIVE_DATA_SUBSTITUTE + + return AnnotatedValue( + value=SENSITIVE_DATA_SUBSTITUTE, + metadata={ + "rem": [ # Remark + [ + "!config", # Because of SDK configuration (in this case the config is the hard coded removal of certain django cookies) + "s", # The fields original value was substituted + ] + ] + }, + ) + if MYPY: from typing import TypeVar diff --git a/tests/integrations/django/test_data_scrubbing.py b/tests/integrations/django/test_data_scrubbing.py new file mode 100644 index 0000000000..c0ab14ae63 --- /dev/null +++ b/tests/integrations/django/test_data_scrubbing.py @@ -0,0 +1,103 @@ +from functools import partial +import pytest +import pytest_django + +from werkzeug.test import Client + +from sentry_sdk.integrations.django import DjangoIntegration + +from tests.integrations.django.myapp.wsgi import application + +try: + from django.urls import reverse +except ImportError: + from django.core.urlresolvers import reverse + + +# Hack to prevent from experimental feature introduced in version `4.3.0` in `pytest-django` that +# requires explicit database allow from failing the test +pytest_mark_django_db_decorator = partial(pytest.mark.django_db) +try: + pytest_version = tuple(map(int, pytest_django.__version__.split("."))) + if pytest_version > (4, 2, 0): + pytest_mark_django_db_decorator = partial( + pytest.mark.django_db, databases="__all__" + ) +except ValueError: + if "dev" in pytest_django.__version__: + pytest_mark_django_db_decorator = partial( + pytest.mark.django_db, databases="__all__" + ) +except AttributeError: + pass + + +@pytest.fixture +def client(): + return Client(application) + + +@pytest.mark.forked +@pytest_mark_django_db_decorator() +def test_scrub_django_session_cookies_removed( + sentry_init, + client, + capture_events, +): + sentry_init(integrations=[DjangoIntegration()], send_default_pii=False) + events = capture_events() + client.set_cookie("localhost", "sessionid", "123") + client.set_cookie("localhost", "csrftoken", "456") + client.set_cookie("localhost", "foo", "bar") + client.get(reverse("view_exc")) + + (event,) = events + assert "cookies" not in event["request"] + + +@pytest.mark.forked +@pytest_mark_django_db_decorator() +def test_scrub_django_session_cookies_filtered( + sentry_init, + client, + capture_events, +): + sentry_init(integrations=[DjangoIntegration()], send_default_pii=True) + events = capture_events() + client.set_cookie("localhost", "sessionid", "123") + client.set_cookie("localhost", "csrftoken", "456") + client.set_cookie("localhost", "foo", "bar") + client.get(reverse("view_exc")) + + (event,) = events + assert event["request"]["cookies"] == { + "sessionid": "[Filtered]", + "csrftoken": "[Filtered]", + "foo": "bar", + } + + +@pytest.mark.forked +@pytest_mark_django_db_decorator() +def test_scrub_django_custom_session_cookies_filtered( + sentry_init, + client, + capture_events, + settings, +): + settings.SESSION_COOKIE_NAME = "my_sess" + settings.CSRF_COOKIE_NAME = "csrf_secret" + + sentry_init(integrations=[DjangoIntegration()], send_default_pii=True) + events = capture_events() + client.set_cookie("localhost", "my_sess", "123") + client.set_cookie("localhost", "csrf_secret", "456") + client.set_cookie("localhost", "foo", "bar") + client.get(reverse("view_exc")) + + (event,) = events + assert event["request"]["cookies"] == { + "my_sess": "[Filtered]", + "csrf_secret": "[Filtered]", + "foo": "bar", + }