Skip to content

Sentry scrubber does not redact X-API-Key, so a 500 can ship a live brz_ credential #3077

Description

@ToddHebebrand

Found while reviewing #2826. Not caused by that PR — pre-existing, and it affects every API-key-authenticated route, so filing separately.

What's wrong: apps/api/src/services/sentry.ts:24-30 scrubs exactly two request headers:

const headers = event?.request?.headers;
if (headers) {
  for (const k of Object.keys(headers)) {
    if (k.toLowerCase() === 'authorization' || k.toLowerCase() === 'cookie') headers[k] = '[redacted]';
  }
}

x-api-key is not in that list. Every API-key-authenticated surface passes its credential in that header — organization API keys (brz_...) and partner service principals (brz_sp_...). @sentry/node's HTTP integration attaches event.request.headers, and app.onError (apps/api/src/index.ts:1003-1020) calls captureException on any unhandled throw. So a single 500 on an API-key route can put a live, unexpired credential into Sentry, where it is retained and visible to anyone with project access.

The intent was already there. The extra scrubber immediately below (sentry.ts:31-40) does know about this credential shape:

if (SENSITIVE_KEYS.has(k.toLowerCase()) || (typeof v === 'string' && v.startsWith('brz_'))) {
  extra[k] = '[redacted]';
}

The brz_ prefix check was written for exactly this class of secret and was never extended to headers. This reads as an oversight in #1379 B3 rather than a deliberate scope decision.

Suggested fix: in scrubEvent, redact by value as well as by name — any header whose value matches /^brz_/, plus x-api-key and x-breeze-csrf by name. Redacting on the value catches future header names for free. scrubEvent is already exported for test, and sentry.test.ts has the harness, so this is a small change with a direct regression test.

Worth checking during the fix: whether any credential has already landed in the retained Sentry events. If so, those keys should be rotated rather than just scrubbed going forward.

Severity: not remotely exploitable on its own — it needs an unhandled 500 on an API-key route — but the payload is a working credential and the blast radius is whoever can read the Sentry project. I'd treat it as a should-fix rather than a drop-everything.

Staying open — no code change yet.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions