Repository: getsentry/sentry-python

Issue:
Check Cookies sent by Django integration
## Summary

Session cookie values logged in Sentry issues, allowing for impersonation if injected into a cookie.

## Cause

The Sentry Python SDK (specifically the Django Integration) sends the cookie values by default. In addition, Sentry SaaS / self-hosted is configured to send_default_pii. 

## Recommended Remediation

The Sentry Python SDK’s Django Integration should be patched to never send the value for [SESSION_COOKIE_NAME](https://docs.djangoproject.com/en/4.1/ref/settings/#session-cookie-name) or [CSRF_COOKIE_NAME](https://docs.djangoproject.com/en/4.1/ref/settings/#std-setting-CSRF_COOKIE_NAME). These values should be scrubbed regardless of the send_default_pii setting.

Other web framework integrations should have similar scrubbing put in place.

The default self-hosted installation configuration should not default to True for send_default_pii. Users are unlikely to change this and it should be opt-in.



Discussion:
The Django integration does not send any cookies over the wire when defaultSendPII=False (this is the default)
So this is "by design" as it is now.

We will have to improve this design.

Resolving patch:
diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py
--- 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
--- 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
--- 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


Test patch:
diff --git a/tests/integrations/django/test_data_scrubbing.py b/tests/integrations/django/test_data_scrubbing.py
new file mode 100644
--- /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",
+    }