fix(lambda): strip Sentry stack-frame locals

This commit is contained in:
Adam Moussa 2026-08-29 15:48:30 -04:00
parent b9c6ec0f78
commit 620e380820
2 changed files with 80 additions and 2 deletions

View file

@ -2,8 +2,10 @@
Imported for side effect from each handler. ``init_sentry()`` is a no-op when
``SENTRY_DSN`` is unset so pytest, local invokes, and a missing HCP var never
talk to Sentry. ``before_send`` strips auth headers and drops request/extra
keys that can hold MIME bodies, Bedrock prompts, or HMAC secret material.
talk to Sentry. ``before_send`` strips auth headers, drops request/extra keys
that can hold MIME bodies, Bedrock prompts, or HMAC secret material, and
removes exception stack-frame locals. ``include_local_variables=False`` keeps
those locals out of the event in the first place.
"""
import os
@ -63,6 +65,38 @@ def _scrub_headers(headers):
return headers
def _stacktraces(event):
traces = []
stacktrace = event.get("stacktrace")
if isinstance(stacktrace, dict):
traces.append(stacktrace)
for section in ("exception", "threads"):
container = event.get(section)
if not isinstance(container, dict):
continue
values = container.get("values")
if not isinstance(values, list):
continue
for item in values:
if not isinstance(item, dict):
continue
inner = item.get("stacktrace")
if isinstance(inner, dict):
traces.append(inner)
return traces
def _strip_stack_locals(event):
"""Drop frame locals. Names like ``raw``/``item`` still hold MIME or secrets."""
for stacktrace in _stacktraces(event):
frames = stacktrace.get("frames")
if not isinstance(frames, list):
continue
for frame in frames:
if isinstance(frame, dict):
frame.pop("vars", None)
def _before_send(event, _hint):
request = event.get("request")
if isinstance(request, dict):
@ -78,6 +112,7 @@ def _before_send(event, _hint):
lower = str(key).lower()
if any(needle in lower for needle in _DROP_EXTRA_NEEDLES):
extra.pop(key, None)
_strip_stack_locals(event)
return event
@ -89,6 +124,7 @@ def init_sentry():
dsn=dsn,
integrations=[AwsLambdaIntegration(timeout_warning=True)],
send_default_pii=False,
include_local_variables=False,
enable_logs=False,
traces_sample_rate=0.0,
before_send=_before_send,

View file

@ -39,6 +39,7 @@ def test_set_dsn_inits_lambda_integration(sentry_mod, monkeypatch):
kwargs = mocked.call_args.kwargs
assert kwargs["dsn"] == "https://key@o1.ingest.sentry.io/1"
assert kwargs["send_default_pii"] is False
assert kwargs["include_local_variables"] is False
assert kwargs["enable_logs"] is False
assert kwargs["traces_sample_rate"] == 0.0
assert kwargs["before_send"] is sentry_mod._before_send
@ -100,3 +101,44 @@ def test_before_send_drops_body_prompt_and_secret_keys(sentry_mod):
assert "bedrock_prompt" not in out["extra"]
assert "hmac_secret" not in out["extra"]
assert out["extra"]["po_number"] == "123"
def test_before_send_drops_exception_and_thread_frame_locals(sentry_mod):
event = {
"exception": {
"values": [
{
"stacktrace": {
"frames": [
{
"function": "handler",
"vars": {
"raw_email": "From: attacker",
"SecretString": "aabbcc",
},
}
]
}
}
]
},
"threads": {
"values": [
{
"stacktrace": {
"frames": [
{"function": "extract_with_claude", "vars": {"prompt": "EXTRACT"}}
]
}
}
]
},
"stacktrace": {
"frames": [{"function": "save_work_order", "vars": {"item": {"wo": 1}}}]
},
}
out = sentry_mod._before_send(event, {})
assert "vars" not in out["exception"]["values"][0]["stacktrace"]["frames"][0]
assert "vars" not in out["threads"]["values"][0]["stacktrace"]["frames"][0]
assert "vars" not in out["stacktrace"]["frames"][0]
assert out["exception"]["values"][0]["stacktrace"]["frames"][0]["function"] == "handler"