Skip to content

Commit 7bbc515

Browse files
committed
1 parent 5e7627c commit 7bbc515

File tree

5 files changed

+284
-0
lines changed

5 files changed

+284
-0
lines changed

sentry_sdk/client.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from sentry_sdk.sessions import SessionFlusher
3030
from sentry_sdk.envelope import Envelope
3131
from sentry_sdk.profiler import has_profiling_enabled, setup_profiler
32+
from sentry_sdk.scrubber import EventScrubber
3233

3334
from sentry_sdk._types import TYPE_CHECKING
3435

@@ -111,6 +112,9 @@ def _get_options(*args, **kwargs):
111112
if rv["enable_tracing"] is True and rv["traces_sample_rate"] is None:
112113
rv["traces_sample_rate"] = 1.0
113114

115+
if rv["event_scrubber"] is None:
116+
rv["event_scrubber"] = EventScrubber()
117+
114118
return rv
115119

116120

@@ -249,6 +253,11 @@ def _prepare_event(
249253
self.options["project_root"],
250254
)
251255

256+
if event is not None:
257+
event_scrubber = self.options["event_scrubber"]
258+
if event_scrubber and not self.options["send_default_pii"]:
259+
event_scrubber.scrub_event(event)
260+
252261
# Postprocess the event here so that annotated types do
253262
# generally not surface in before_send
254263
if event is not None:

sentry_sdk/consts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ def __init__(
133133
trace_propagation_targets=[ # noqa: B006
134134
MATCH_ALL
135135
], # type: Optional[Sequence[str]]
136+
event_scrubber=None, # type: Optional[sentry_sdk.scrubber.EventScrubber]
136137
):
137138
# type: (...) -> None
138139
pass

sentry_sdk/scrubber.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from sentry_sdk.utils import (
2+
capture_internal_exceptions,
3+
AnnotatedValue,
4+
iter_event_frames,
5+
)
6+
from sentry_sdk._compat import string_types
7+
from sentry_sdk._types import TYPE_CHECKING
8+
9+
if TYPE_CHECKING:
10+
from sentry_sdk._types import Event
11+
from typing import Any
12+
from typing import Dict
13+
from typing import List
14+
from typing import Optional
15+
16+
17+
DEFAULT_DENYLIST = [
18+
# stolen from relay
19+
"password",
20+
"passwd",
21+
"secret",
22+
"api_key",
23+
"apikey",
24+
"auth",
25+
"credentials",
26+
"mysql_pwd",
27+
"privatekey",
28+
"private_key",
29+
"token",
30+
"ip_address",
31+
"session",
32+
# django
33+
"csrftoken",
34+
"sessionid",
35+
# wsgi
36+
"remote_addr",
37+
"x_csrftoken",
38+
"x_forwarded_for",
39+
"set_cookie",
40+
"cookie",
41+
"authorization",
42+
"x_api_key",
43+
"x_forwarded_for",
44+
"x_real_ip",
45+
]
46+
47+
48+
class EventScrubber(object):
49+
def __init__(self, denylist=None):
50+
# type: (Optional[List[str]]) -> None
51+
self.denylist = DEFAULT_DENYLIST if denylist is None else denylist
52+
53+
def scrub_dict(self, d):
54+
# type: (Dict[str, Any]) -> None
55+
if not isinstance(d, dict):
56+
return
57+
58+
for k in d.keys():
59+
if isinstance(k, string_types) and k.lower() in self.denylist:
60+
d[k] = AnnotatedValue.substituted_because_contains_sensitive_data()
61+
62+
def scrub_request(self, event):
63+
# type: (Event) -> None
64+
with capture_internal_exceptions():
65+
if "request" in event:
66+
if "headers" in event["request"]:
67+
self.scrub_dict(event["request"]["headers"])
68+
if "cookies" in event["request"]:
69+
self.scrub_dict(event["request"]["cookies"])
70+
if "data" in event["request"]:
71+
self.scrub_dict(event["request"]["data"])
72+
73+
def scrub_extra(self, event):
74+
# type: (Event) -> None
75+
with capture_internal_exceptions():
76+
if "extra" in event:
77+
self.scrub_dict(event["extra"])
78+
79+
def scrub_user(self, event):
80+
# type: (Event) -> None
81+
with capture_internal_exceptions():
82+
if "user" in event:
83+
self.scrub_dict(event["user"])
84+
85+
def scrub_breadcrumbs(self, event):
86+
# type: (Event) -> None
87+
with capture_internal_exceptions():
88+
if "breadcrumbs" in event:
89+
if "values" in event["breadcrumbs"]:
90+
for value in event["breadcrumbs"]["values"]:
91+
if "data" in value:
92+
self.scrub_dict(value["data"])
93+
94+
def scrub_frames(self, event):
95+
# type: (Event) -> None
96+
with capture_internal_exceptions():
97+
for frame in iter_event_frames(event):
98+
if "vars" in frame:
99+
self.scrub_dict(frame["vars"])
100+
101+
def scrub_spans(self, event):
102+
# type: (Event) -> None
103+
with capture_internal_exceptions():
104+
if "spans" in event:
105+
for span in event["spans"]:
106+
if "data" in span:
107+
self.scrub_dict(span["data"])
108+
109+
def scrub_event(self, event):
110+
# type: (Event) -> None
111+
self.scrub_request(event)
112+
self.scrub_extra(event)
113+
self.scrub_user(event)
114+
self.scrub_breadcrumbs(event)
115+
self.scrub_frames(event)
116+
self.scrub_spans(event)

sentry_sdk/serializer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,8 @@ def _serialize_node_impl(
254254
obj, is_databag, should_repr_strings, remaining_depth, remaining_breadth
255255
):
256256
# type: (Any, Optional[bool], Optional[bool], Optional[int], Optional[int]) -> Any
257+
if isinstance(obj, AnnotatedValue):
258+
should_repr_strings = False
257259
if should_repr_strings is None:
258260
should_repr_strings = _should_repr_strings()
259261

tests/test_scrubber.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import sys
2+
import pytest
3+
import logging
4+
5+
from sentry_sdk import capture_exception, capture_event, start_transaction, start_span
6+
from sentry_sdk.utils import event_from_exception
7+
from sentry_sdk.scrubber import EventScrubber
8+
9+
10+
logger = logging.getLogger(__name__)
11+
logger.setLevel(logging.DEBUG)
12+
13+
14+
def test_request_scrubbing(sentry_init, capture_events):
15+
sentry_init()
16+
events = capture_events()
17+
18+
try:
19+
1 / 0
20+
except ZeroDivisionError:
21+
ev, _hint = event_from_exception(sys.exc_info())
22+
23+
ev["request"] = {
24+
"headers": {
25+
"COOKIE": "secret",
26+
"authorization": "Bearer bla",
27+
"ORIGIN": "google.com",
28+
},
29+
"cookies": {
30+
"sessionid": "secret",
31+
"foo": "bar",
32+
},
33+
"data": {
34+
"token": "secret",
35+
"foo": "bar",
36+
},
37+
}
38+
39+
capture_event(ev)
40+
41+
(event,) = events
42+
43+
assert event["request"] == {
44+
"headers": {
45+
"COOKIE": "[Filtered]",
46+
"authorization": "[Filtered]",
47+
"ORIGIN": "google.com",
48+
},
49+
"cookies": {"sessionid": "[Filtered]", "foo": "bar"},
50+
"data": {"token": "[Filtered]", "foo": "bar"},
51+
}
52+
53+
assert event["_meta"]["request"] == {
54+
"headers": {
55+
"COOKIE": {"": {"rem": [["!config", "s"]]}},
56+
"authorization": {"": {"rem": [["!config", "s"]]}},
57+
},
58+
"cookies": {"sessionid": {"": {"rem": [["!config", "s"]]}}},
59+
"data": {"token": {"": {"rem": [["!config", "s"]]}}},
60+
}
61+
62+
63+
def test_stack_var_scrubbing(sentry_init, capture_events):
64+
sentry_init()
65+
events = capture_events()
66+
67+
try:
68+
password = "supersecret" # noqa
69+
api_key = "1231231231" # noqa
70+
safe = "keepthis" # noqa
71+
1 / 0
72+
except ZeroDivisionError:
73+
capture_exception()
74+
75+
(event,) = events
76+
77+
frames = event["exception"]["values"][0]["stacktrace"]["frames"]
78+
(frame,) = frames
79+
assert frame["vars"]["password"] == "[Filtered]"
80+
assert frame["vars"]["api_key"] == "[Filtered]"
81+
assert frame["vars"]["safe"] == "'keepthis'"
82+
83+
meta = event["_meta"]["exception"]["values"]["0"]["stacktrace"]["frames"]["0"][
84+
"vars"
85+
]
86+
assert meta == {
87+
"password": {"": {"rem": [["!config", "s"]]}},
88+
"api_key": {"": {"rem": [["!config", "s"]]}},
89+
}
90+
91+
92+
def test_breadcrumb_extra_scrubbing(sentry_init, capture_events):
93+
sentry_init()
94+
events = capture_events()
95+
96+
logger.info("bread", extra=dict(foo=42, password="secret"))
97+
logger.critical("whoops", extra=dict(bar=69, auth="secret"))
98+
99+
(event,) = events
100+
101+
assert event["extra"]["bar"] == 69
102+
assert event["extra"]["auth"] == "[Filtered]"
103+
104+
assert event["breadcrumbs"]["values"][0]["data"] == {
105+
"foo": 42,
106+
"password": "[Filtered]",
107+
}
108+
109+
assert event["_meta"] == {
110+
"extra": {"auth": {"": {"rem": [["!config", "s"]]}}},
111+
"breadcrumbs": {
112+
"values": {"0": {"data": {"password": {"": {"rem": [["!config", "s"]]}}}}}
113+
},
114+
}
115+
116+
117+
def test_span_data_scrubbing(sentry_init, capture_events):
118+
sentry_init(traces_sample_rate=1.0)
119+
events = capture_events()
120+
121+
with start_transaction(name="hi"):
122+
with start_span(op="foo", description="bar") as span:
123+
span.set_data("password", "secret")
124+
span.set_data("datafoo", "databar")
125+
126+
(event,) = events
127+
assert event["spans"][0]["data"] == {"password": "[Filtered]", "datafoo": "databar"}
128+
assert event["_meta"] == {
129+
"spans": {"0": {"data": {"password": {"": {"rem": [["!config", "s"]]}}}}}
130+
}
131+
132+
133+
def test_custom_denylist(sentry_init, capture_events):
134+
sentry_init(event_scrubber=EventScrubber(denylist=["my_sensitive_var"]))
135+
events = capture_events()
136+
137+
try:
138+
my_sensitive_var = "secret" # noqa
139+
safe = "keepthis" # noqa
140+
1 / 0
141+
except ZeroDivisionError:
142+
capture_exception()
143+
144+
(event,) = events
145+
146+
frames = event["exception"]["values"][0]["stacktrace"]["frames"]
147+
(frame,) = frames
148+
assert frame["vars"]["my_sensitive_var"] == "[Filtered]"
149+
assert frame["vars"]["safe"] == "'keepthis'"
150+
151+
meta = event["_meta"]["exception"]["values"]["0"]["stacktrace"]["frames"]["0"][
152+
"vars"
153+
]
154+
assert meta == {
155+
"my_sensitive_var": {"": {"rem": [["!config", "s"]]}},
156+
}

0 commit comments

Comments
 (0)