fix(api): scrub response bodies and PII-keyed extras from Sentry events#337
Conversation
…ts (#243) The BeforeSend scrubber cleared the request body, headers and cookies plus top-level user PII, but never touched the response context (its body, cookies and headers), the request query string, the user Other dictionary, or sensitive keys in the Extra bag. A captured event could therefore still ship a response body or PII-keyed extra to Sentry. Extract the scrubber into SentryEventScrubber and extend it to strip the response context, the request query string and user.Other, and to redact Extra entries whose keys match known-PII fragments (benign debug context is preserved). Add focused unit tests for each vector plus the no-op path. Refs thomasluizon/orbit-ui-mobile#243 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review: PR #337 (thomasluizon/orbit-api)
Scope: PR #337 — fix(api): scrub response bodies and PII-keyed extras from Sentry events
Recommendation: APPROVE
Summary
This PR extracts the Sentry BeforeSend scrubber into a focused, unit-tested SentryEventScrubber and closes four real, previously-unscrubbed PII vectors: response context (body/cookies/headers), request query string, SentryUser.Other, and key-matched Extra entries. The diff is small (3 files), internal-only (no DTO/Controller/contract surface), and backed by 6 targeted unit tests. No prior review threads or bot reviews exist on this PR to reconcile against.
Findings
Critical
None.
High
None. (A security-reviewer subagent initially flagged unscrubbed Sentry Breadcrumbs as High. An adversarial skeptic pass investigated the specific claimed mechanisms against this codebase and refuted them: no Sentry.EntityFrameworkCore package is referenced anywhere in the solution, so EF breadcrumbs don't exist; the more plausible mechanism — ASP.NET Core request-logging breadcrumbs carrying the incoming query string — is already suppressed by the pre-existing "Microsoft.AspNetCore": "Warning" log-level filter in appsettings.json/appsettings.Production.json. The gap is also pre-existing (the old scrubber never touched Breadcrumbs either) and doesn't meet the Critical bar. Dropped per the verification protocol.)
Medium
None. (A candidate — SensitiveKeyFragments in src/Orbit.Api/Observability/SentryEventScrubber.cs:10-16 lacks a bare "key" fragment, so a hypothetical Extra key like "stripeKey" would slip through — was considered but dropped as speculative: grep -rn "SetExtra" across src/ shows the only call site is inside the scrubber itself; there is no current code path that would set such a key. Per the rubric's signal gate, Medium findings must be concretely actionable, not speculative.)
Low / Info
None posted (per rubric signal gate).
Subagents
| Agent | Verdict |
|---|---|
| security-reviewer | PASS (one High finding raised, refuted by adversarial skeptic and dropped — see High section) |
| contract-aligner | N/A — no DTO, Controller route, or packages/shared surface changed in this diff |
Validation
| Check | Result |
|---|---|
| Build (dotnet) | N/A — skipped in CI job, covered by separate required check |
| Tests (dotnet) | N/A — skipped in CI job, covered by separate required check |
Deferred — N/A dimensions & files not verdicted
- DESIGN.md/AI-slop (#8) — N/A, no
apps/*UI files touched. - Parity (#9) — N/A, backend-only change, no
apps/web/apps/mobilefiles. - i18n (#10) — N/A, no new user-facing strings.
- Contract drift + backward-compat guard (#11) — N/A, no DTO/Controller/
packages/sharedchange; Sentry telemetry scrubbing is server-internal and invisible to any client contract, confirmed via diff (onlyServiceCollectionExtensions.cs,SentryEventScrubber.cs, and a test file changed). - Backend hard rules (#13) — Timezone/Authorization/Validation/Transaction teardown — N/A, no surface touched (no dates, no endpoints, no transactions in this diff).
- FEATURES.md parity (#14) — N/A, no user-facing feature surface changed (internal hardening only, matches PR body's own scope statement); not verifiable in this checkout regardless.
- Cross-repo dimensions requiring
orbit-ui-mobile— not verifiable in this CI job (sibling repo not checked out), per task instructions. Confirmed N/A anyway since no contract surface changed. - Second-opinion cross-model step (verification protocol §2.2) — no Critical finding survived to trigger it; UNAVAILABLE would apply regardless (no
opencodein CI).
All three changed files (ServiceCollectionExtensions.cs, Observability/SentryEventScrubber.cs, tests/.../SentryEventScrubberTests.cs) received an explicit verdict — nothing in the diff went unexamined.
What's good
- Genuine root-cause fix: closes four concretely identified PII gaps (response context, query string,
User.Other, key-matchedExtra), each with its own unit test. ScrubResponsecorrectly iterates existingContexts.Valuesrather than the lazyContexts.Responseaccessor, avoiding fabricating an empty response context on clean events — verified by its own test (Scrub_DoesNotAddResponseContext_WhenNonePresent).- Clean extraction: old dead
ScrubSensitiveDatafully removed with zero orphan references (verified via repo-wide grep); newinternal static classcorrectly reuses the existingInternalsVisibleTo Orbit.Infrastructure.Testsgrant rather than widening visibility. - Zero narration comments in either new file (ORBIT0001-clean).
- Test placement matches the tests/CLAUDE.md convention given no
Orbit.Api.Testsproject exists.
Recommendation
Approve as-is. Optional non-blocking follow-up (not required for this PR): consider scrubbing sentryEvent.Breadcrumbs in a future hardening pass for defense-in-depth against future integrations (e.g., if Sentry.EntityFrameworkCore or HttpClient breadcrumb capture is ever added), but nothing in the current codebase makes this an active leak.
|
There was a problem hiding this comment.
PR Review — #337: fix(api): scrub response bodies and PII-keyed extras from Sentry events
Recommendation: APPROVE
| Severity | Count |
|---|---|
| Critical (incl. breaks old mobile clients) | 0 |
| High | 0 |
| Medium | 0 |
| Low / Info | 0 |
Basis: The PR head (8f8b107b) is identical to the commit already reviewed by this bot in a prior pass — no new commits have landed since. Re-running the /pr-review skill against the current diff found nothing new to examine, so the prior findings stand:
- Genuine root-cause fix closing four PII leak paths in Sentry event capture: response context, request query string,
SentryUser.Other, and key-matchedExtrafields — each covered by a dedicated unit test intests/Orbit.Infrastructure.Tests/Observability/SentryEventScrubberTests.cs. - No DTO/Controller/contract-surface change, so the cross-repo contract-parity dimension is not applicable (and not verifiable in this CI job regardless, since
orbit-ui-mobileisn't checked out here). - A security-reviewer-raised High (potentially unscrubbed Sentry Breadcrumbs) was investigated and refuted: no
Sentry.EntityFrameworkCorepackage is referenced in the solution, and ASP.NET Core request-logging breadcrumbs are already suppressed by the existingMicrosoft.AspNetCore: Warninglog filter — correctly dropped as a false positive, not re-flagged.
No new Critical/High/Medium findings to report.



What
The Sentry
BeforeSendscrubber (ServiceCollectionExtensions.ScrubSensitiveData) cleared the request body, headers and cookies plus top-level user PII, but left several sensitive vectors untouched. Verified against the Sentry .NET SDK 6.6.0 protocol types (SentryEvent,SentryUser,SentryRequest,SentryContexts.Response), the gaps were:sentryEvent.Contexts.Response(itsData,Cookies,Headers) was never scrubbed. This is the response-body gap called out for verification.Request.QueryStringcan carry tokens/emails and was not cleared.Otherdictionary — arbitrary user PII survived (onlyEmail/Username/IpAddresswere cleared).Extrabag — no key-based PII redaction, so a known-PII-keyed extra (e.g.authToken,userEmail) shipped verbatim.Change
Orbit.Api.Observability.SentryEventScrubber(internal;Orbit.Apialready exposes internals toOrbit.Infrastructure.Tests).Other; request body/headers/cookies/query-string; the response context (body/cookies/headers); and redactsExtraentries whose keys match known-PII fragments while preserving benign debug context.ScrubResponseiterates existing contexts rather than the lazyContexts.Responseaccessor, so it never fabricates an empty response context on clean events.Tests
Added
SentryEventScrubberTests(6 focused cases): response body/headers/cookies stripped; no response context fabricated when absent; user PII incl.Othercleared; request body/cookies/headers/query-string cleared; known-PIIExtrakeys redacted while benign keys survive; and the no-op path returns the same event. Full suite green (Domain 504, Analyzers 7, Application 2628, Infrastructure 1540).Scope
API-internal observability only — no endpoint or DTO contract change, so no
packages/shared/ consumer edits apply (parity-nudge false positive).Refs thomasluizon/orbit-ui-mobile#243