Skip to content

fix(sentry): cap request bodies at 1KB and scrub Postgres DETAIL rows - #5561

Open
blarghmatey wants to merge 3 commits into
masterfrom
tmacey/sentry-scrub-request-bodies
Open

fix(sentry): cap request bodies at 1KB and scrub Postgres DETAIL rows#5561
blarghmatey wants to merge 3 commits into
masterfrom
tmacey/sentry-scrub-request-bodies

Conversation

@blarghmatey

@blarghmatey blarghmatey commented Sep 8, 2026

Copy link
Copy Markdown
Member

This repo

This app never passed the body-size option, so it has been on the SDK default of "medium" — 10KB bodies. Sets it to "small" explicitly, and adds the Postgres DETAIL: scrub to before_send.

⚠️ Spelled request_bodies, not max_request_body_size. This app is pinned to sentry-sdk==1.9.0 (pyproject.toml:38, and uv.lock agrees), where that is the option name — the default is in ClientConstructor and the bounds check is request_body_within_bounds. It was renamed in 2.x. init validates its kwargs strictly and raises TypeError: Unknown option 'max_request_body_size' on an unknown one (_get_options), so the 2.x spelling would take Sentry down entirely on this app rather than being silently ignored — hit while writing this change, so it is observed, not predicted. Every other repo in this ten-repo change uses the 2.x name. Rename when the pin moves; tracked at the micromasters SDK-bump task.

The estate spans sentry-sdk 1.9.0 (here) to 2.68.0, so this is the one repo where the uniform edit does not apply verbatim.

The scrub helpers are module-level here, unlike before_send which is nested inside init_sentry, so they are testable without Django settings. For the same reason, the SDK-driven test can't go through this app's before_send; see Testing.

Production runs Django under WSGI (granian --interface wsgi), so the Content-Length caveat below doesn't apply.

What

Two independent ways learner data reaches Sentry, neither of them governed by send_default_pii. Part of a ten-repo change on branch tmacey/sentry-scrub-request-bodies.

1. HTTP request bodies are captured with no PII gate

request_info["data"] is set unconditionally in RequestExtractor.extract_into_event. The only control is max_request_body_size, checked by request_body_within_bounds:

value effect
never no bodies
small bodies <= 1,000 bytes
medium bodies <= 10,000 bytes — the SDK default
always no limit

Verified against sentry-sdk source: request_body_within_bounds does the bounds check, RequestExtractor.extract_into_event sets the body unconditionally, and "medium" is the max_request_body_size default. The repos in this change pin 1.9.0 to 2.68.0, and the behaviour is the same across them.

The bound is checked against the declared Content-Length, and a missing or malformed header counts as 0. Under WSGI, Django then reads 0 bytes, so nothing gets past it. Under ASGI, Django takes CONTENT_LENGTH from the header and reads the whole body, so on learn-ai and mit-learn a chunked request with no Content-Length is captured in full. The Starlette integration (ol-analytics-api) attaches no body when the header is absent.

Which endpoints that actually means, measured in Sentry over the 30 days to 2026-09-08 (dataset=errors, http.method:[POST,PUT,PATCH], grouped by transaction):

transaction project events
PATCH /scim/v2/Users/{uuid} mitxonline 17,610
POST /api/v1/webhooks/content_files/ mit-learn 5,039
POST /api/v1/enrollments/ mitxonline 1,436
POST .../xblock/{usage_id}/handler/{handler}/ openedx-mitxpro 553
POST /cms/pages/{page_id}/edit/ micromasters 273
POST /api/profile/details/ mitxonline 132
POST /api/checkout/result/ mitxonline 65
POST /api/checkout/redeem_discount/ mitxonline 46

Enrollment, checkout, profile edits, SCIM user attributes, and xblock handler submissions — governed by a knob nobody set.

2. Postgres DETAIL: lines leak rows through the exception text

A constraint violation carries a DETAIL: Failing row contains (...) line reproducing the whole offending row, and psycopg puts it in str(exc). It therefore ships inside the exception value, where no SDK privacy option reaches it — send_default_pii governs user/cookie/header capture and max_request_body_size governs bodies; neither touches exception text.

Measured on MITXONLINE-6PK: a SCIM PATCH IntegrityError on users_user whose DETAIL line reproduces a learner email address three times per event. First seen 2026-05-27, 46,764 occurrences, last seen 2026-09-08 — still firing.

before_send now walks the whole serialized event and truncates any string at its DETAIL: line, whether the newline arrives raw or as a literal \n (the SDK repr()s frame locals and non-string log params before before_send runs). It keeps the primary error that names the failure and drops the row echo plus any HINT/CONTEXT after it.

Testing

Unit tests next to the module use a real MITXONLINE-6PK exception value with the learner identifiers replaced. They assert the primary message survives, the email and external UUID do not, and HINT/CONTEXT are dropped, for both the raw DETAIL: line and the repr'd form the SDK produces for frame locals and log params. One test goes through a real sentry-sdk 1.9.0 client with a fake transport and checks that no outgoing event carries the email. before_send is nested inside init_sentry here, so that test passes the scrub to the client directly. Against the pre-review module, 4 of the tests fail, that one included.

Not in scope

send_default_pii and the salted-user-hash work are deliberately untouched here — different knob, different failure mode, tracked separately.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RVJKTGk9KHUTN2xfU59ujX

The other nine PRs

Same branch name (tmacey/sentry-scrub-request-bodies) in each repo. Independent — no merge order required.

repo PR body size
open-edx-plugins mitodl/open-edx-plugins#866 never
mit-learn mitodl/mit-learn#3915 small
mitxonline mitodl/mitxonline#3936 small
mitxpro mitodl/mitxpro#4088 small
ocw-studio mitodl/ocw-studio#3198 small
odl-video-service mitodl/odl-video-service#1591 small
micromasters #5561 small (as request_bodies, SDK 1.9.0)
open-discussions mitodl/open-discussions#4463 small
learn-ai mitodl/learn-ai#62 small
ol-analytics-api mitodl/ol-analytics-api#54 small

Two ways learner data reaches Sentry, neither gated by send_default_pii.

Request bodies. The SDK sets request.data unconditionally and the body
size option is the only control; left unset it defaults to "medium",
i.e. 10,000-byte bodies. Nobody chose that. This app raises on POSTs to
/cms/pages/{page_id}/edit/ (273 events in the last 30 days), so the
capture is not theoretical. Set to "small" explicitly.

Spelled `request_bodies` rather than `max_request_body_size` because
this app is pinned to sentry-sdk==1.9.0, where that is the option name
(consts.py:70, _wsgi_common.py:39); it was renamed in 2.x. init
validates kwargs strictly and raises TypeError on an unknown one
(client.py:61), so the 2.x spelling would take Sentry down on this app
rather than being ignored. Every other application in the estate uses
the 2.x name. Rename when the pin moves.

Postgres DETAIL lines. A constraint violation carries a DETAIL line that
echoes the whole offending row, and psycopg puts it in str(exc) -- so it
ships inside the exception value, which no SDK privacy option covers.
Measured on mitxonline MITXONLINE-6PK: a SCIM PATCH IntegrityError
reproducing a learner email address three times per event, 46,764
occurrences since 2026-05-27. before_send now truncates at the DETAIL
marker across exception values, logentry, and the legacy top-level
message, keeping the primary error that names the failure.

The scrub helpers are module-level, unlike before_send which is nested
inside init_sentry here, so they can be tested without Django settings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RVJKTGk9KHUTN2xfU59ujX
@blarghmatey
blarghmatey force-pushed the tmacey/sentry-scrub-request-bodies branch from f8997cd to c773bc4 Compare September 8, 2026 15:41
blarghmatey and others added 2 commits September 8, 2026 11:54
…fields

Copilot review, verified against sentry-sdk 2.55.0 source. The first pass
enumerated three paths -- exception values, logentry.message/.formatted,
and the legacy top-level message -- and missed every other field that can
carry the same string:

  breadcrumbs[].message   LoggingIntegration records each log record as a
                          breadcrumb (integrations/logging.py:311). This is
                          exactly MITXONLINE-6PK's shape: mechanism=logging,
                          logger=django_scim.views.
  logentry.params         record.args verbatim (:274), so
                          logger.error("...: %s", exc) carries it.
  frames[].vars           include_local_variables defaults to True
                          (consts.py:1028, utils.py:616), so a catch block
                          holding the exception in a local carries it.

Confirmed the old implementation leaked on all three shapes before
changing it; the new tests fail against it and pass against the walk.

Replaced with a recursive walk of the event instead of a longer path
list -- it covers these without enumerating them and does not go stale
when the SDK grows another such field. The walk only rewrites str leaves
and preserves everything else, with a test pinning that.

Copilot also suggested normalizing exception-valued params. Not needed:
client._prepare_event serializes the event before calling before_send
(client.py:650 vs :658), so every leaf is already a JSON primitive by
then and there are no live exception objects left to coerce.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RVJKTGk9KHUTN2xfU59ujX
The SDK repr()s frame locals and non-string logging params before
before_send, so there the DETAIL line arrives with a literal backslash-n
and the old find("\nDETAIL:") missed it. Match both forms, and test
through a real sentry-sdk 1.9.0 client instead of hand-built events.

Cite SDK function names instead of line numbers, which drift, and drop
the tuple branch: the serializer turns tuples into lists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MYc2F3cFvSCfVzqeRMshGy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant