From 620e380820a58ade62280265a1601a73445e2994 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Sat, 29 Aug 2026 15:48:30 -0400 Subject: [PATCH] fix(lambda): strip Sentry stack-frame locals --- lambdas/shared/sentry_init.py | 40 +++++++++++++++++++++++++++++++-- tests/test_sentry_init.py | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/lambdas/shared/sentry_init.py b/lambdas/shared/sentry_init.py index 4458744..0b07a7c 100644 --- a/lambdas/shared/sentry_init.py +++ b/lambdas/shared/sentry_init.py @@ -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, diff --git a/tests/test_sentry_init.py b/tests/test_sentry_init.py index e41667c..b08a1fc 100644 --- a/tests/test_sentry_init.py +++ b/tests/test_sentry_init.py @@ -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"