Skip to content

fix(core): Apply the sensitive denylist to cookie headers and configured fetch headers - #24090

Open
s1gr1d wants to merge 7 commits into
developfrom
sig/dc-header-cookie-denylist
Open

s1gr1d wants to merge 7 commits into
developfrom
sig/dc-header-cookie-denylist

Conversation

@s1gr1d

@s1gr1d s1gr1d commented Sep 4, 2026

Copy link
Copy Markdown
Member

Three ways a sensitive value slipped past the denylist, now that cookies ship as one array attribute (#24231).

A cookie segment without an = is a nameless cookie, so the bare token is its value (RFC 6265bis). The SDK treated it as a name, and no name-based denylist can match a value, so Cookie: <session-token> shipped the token in the clear. Such segments now become a [Filtered] array element. The Cookie header was also split on "; ", but the space is not guaranteed on the wire, so a cookie glued on with a bare ; leaked inside the previous cookie's value. The split is now on ";".

Headers listed in headersToSpanAttributes skipped the denylist entirely, so authorization went out in the clear. The spec says an allowlist never exempts a sensitive name, so those now emit ['[Filtered]'].

Fixes #24085

…red fetch headers

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 29.09 kB - -
@sentry/browser - with treeshaking flags 27.35 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.26 kB - -
@sentry/browser (incl. Tracing) 50.6 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 50.62 kB - -
@sentry/browser (incl. Tracing, Profiling) 53.61 kB - -
@sentry/browser (incl. Tracing, Replay) 90.15 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 79.25 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 94.85 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 107.83 kB - -
@sentry/browser (incl. Feedback) 46.62 kB - -
@sentry/browser (incl. sendFeedback) 34.15 kB - -
@sentry/browser (incl. FeedbackAsync) 39.26 kB - -
@sentry/browser (incl. Metrics) 30.1 kB - -
@sentry/browser (incl. Logs) 30.35 kB - -
@sentry/browser (incl. Metrics & Logs) 31.02 kB - -
@sentry/react 30.84 kB - -
@sentry/react (incl. Tracing) 52.94 kB - -
@sentry/vue 36.34 kB - -
@sentry/vue (incl. Tracing) 52.91 kB - -
@sentry/svelte 29.11 kB - -
CDN Bundle 30.8 kB - -
CDN Bundle (incl. Tracing) 51.15 kB - -
CDN Bundle (incl. Logs, Metrics) 33.06 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 53.14 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 73.75 kB - -
CDN Bundle (incl. Tracing, Replay) 88.69 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 90.63 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 94.73 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 96.78 kB - -
CDN Bundle - uncompressed 91.16 kB - -
CDN Bundle (incl. Tracing) - uncompressed 152.66 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 97.73 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 158.61 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 227.14 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 272.23 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 278.17 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 285.93 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 291.86 kB - -
@sentry/nextjs (client) 55.27 kB - -
@sentry/sveltekit (client) 51.05 kB - -
@sentry/core/server 39.66 kB +0.07% +26 B 🔺
@sentry/core/browser 13.66 kB - -
@sentry/node 133.29 kB +0.02% +17 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 82.11 kB - -
@sentry/node - without tracing 89.88 kB +0.03% +19 B 🔺
@sentry/node - without channel injection 112.15 kB +0.02% +12 B 🔺
@sentry/aws-serverless 98.16 kB +0.03% +23 B 🔺
@sentry/cloudflare (withSentry) - minified 204.74 kB +0.03% +44 B 🔺
@sentry/cloudflare (withSentry) 509.41 kB +0.04% +155 B 🔺

View base workflow run

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@s1gr1d
s1gr1d marked this pull request as ready for review September 4, 2026 13:41
@s1gr1d
s1gr1d requested review from a team as code owners September 4, 2026 13:41
@s1gr1d
s1gr1d requested review from isaacs and mydea and removed request for a team September 4, 2026 13:41
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@s1gr1d

s1gr1d commented Sep 7, 2026

Copy link
Copy Markdown
Member Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/core/src/utils/request.ts Outdated
*/
function parseCookieHeader(value: string, isSetCookie: boolean): Record<string, string> | undefined {
// Set-Cookie: single cookie with attributes ("name=value; HttpOnly; Secure")
// Cookie: multiple cookies separated by "; " ("cookie1=value1; cookie2=value2")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: The split is on '; ' (semicolon plus space), so a header without the space could still leak.

Cookie: theme=dark;__Secure-session=abc123
-> { 'http.request.header.cookie.theme': 'dark;__Secure-session=abc123' }

Could we use parseCookie and drop this function entirely? It seems to cover all the cases we need to support.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parseCookie function does some things a bit differently. Like it does not accept nameless segments and it dedupes cookie names.

The rest is addressed and there's a test for it.

// A non-empty string we cannot parse may still hold a session token, so it counts as sensitive.
if (Object.keys(parsed).length === 0) {
return {};
return cookieString ? FILTERED : {};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: This might be a no-op, both httpclient.ts call sites check for an object

const filtered = _INTERNAL_filterCookies(reqCookieStr, dc.cookies);
if (typeof filtered === 'object') {
  requestCookies = filtered;
}

So it would be dropped rather than show up as [FILTERED], maybe we need to adjust those checks as well?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, came to say a similar thing:

filterCookies('opaque-blob; theme=dark', true) => {"theme":"dark"}
httpHeadersToSpanAttributes({Cookie: 'opaque-blob; theme=dark'}) => ["[Filtered]","theme=dark"]

It seems like we should maybe call out that non-key=value-parseable cookie segments are just dropped?

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

👋 @isaacs, @mydea — Please review this PR when you get a chance!

@s1gr1d
s1gr1d marked this pull request as draft September 10, 2026 12:03
@s1gr1d

s1gr1d commented Sep 10, 2026

Copy link
Copy Markdown
Member Author

Will wait on the cookie changes Lukas is working on until I rebase this.

# Conflicts:
#	packages/core/src/utils/request.ts
#	packages/core/test/lib/utils/request.test.ts
@s1gr1d
s1gr1d marked this pull request as ready for review September 17, 2026 09:55
@s1gr1d
s1gr1d requested a review from a team as a code owner September 17, 2026 09:55
@s1gr1d
s1gr1d requested review from logaretm and removed request for a team September 17, 2026 09:55
@github-actions

Copy link
Copy Markdown
Contributor

👋 @isaacs, @mydea — Please review this PR when you get a chance!

@s1gr1d
s1gr1d requested review from JPeer264 and Lms24 September 17, 2026 11:03

@isaacs isaacs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, the design and intention here is correct, and it does fix a lot of edge cases that were unhandled (or handled improperly) before.

I think we can consolidate a few things to make it even cleaner, but that doesn't need to block landing this, imo.

const attrValue = Array.isArray(value) ? value : [value];
spanAttributes[`http.request.header.${name}`] = attrValue;
// An allowlist entry does not exempt a header from the denylist.
spanAttributes[`http.request.header.${name}`] = _INTERNAL_shouldFilterDataKey(name, true)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could be misunderstanding this, but it seems like _INTERNAL_shouldFilterDataKey(name, true) hardcodes true as the behavior, so only the built-in denylist applies. The user's own configuration is still bypassed:

  • dataCollection: { httpHeaders: { request: false } } means "collect no request headers". Every header listed in headersToSpanAttributes is still attached to the span.
  • dataCollection: { httpHeaders: { request: { deny: ['x-tenant-id'] } } } is not consulted, so an allowlisted x-tenant-id still goes out.

Can we pass the resolved behavior instead of true? getClient() is already imported and used in this file (on line 262), so client.getDataCollectionOptions().httpHeaders.request seems like an option.

One side effect of that: with a user behavior of { allow: [...] }, shouldFilterDataKey filters everything not in allow, which would nerf headersToSpanAttributes unless the same names appear in both lists. If that is too aggressive, we could honor false and deny only, and say so in the JSDoc.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, also: couldn't this loop, and the one on lines 363-387, reuse httpHeadersToSpanAttributes?

Something like this:

const allowlisted: Record<string, string | string[]> = {};
for (const [name, value] of headersMap.entries()) {
  if (headersToAttribs.has(name)) {
    allowlisted[name] = value;
  }
}
Object.assign(
  spanAttributes,
  httpHeadersToSpanAttributes(allowlisted, client.getDataCollectionOptions(), 'request'),
);

But like I said, I could be misunderstanding the reason for the divergence, so if there's a reason to handle it separately, please ignore :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I implemented your second suggestion!

// A non-empty string we cannot parse may still hold a session token, so it counts as sensitive.
if (Object.keys(parsed).length === 0) {
return {};
return cookieString ? FILTERED : {};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, came to say a similar thing:

filterCookies('opaque-blob; theme=dark', true) => {"theme":"dark"}
httpHeadersToSpanAttributes({Cookie: 'opaque-blob; theme=dark'}) => ["[Filtered]","theme=dark"]

It seems like we should maybe call out that non-key=value-parseable cookie segments are just dropped?

return spanAttributes;
}

/**

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's probably out of scope for this PR, and can definitely be filed as a followup. But, as of this, we have 2 cookie parsers that disagree subtly.

parseCookieHeader (this file) and parseCookie (packages/core/src/utils/cookie.ts line 34, used by filterCookies) differ on nameless segments, URL-decoding, quote-stripping, and Set-Cookie attributes.

One parser returning [name, value][] would let filterCookies and httpHeadersToSpanAttributes share the same rules. Recommend filing it as follow-up rather than growing this patch.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this is kind of weird:

filterCookies('sid=1; Max-Age=3600; Path=/', true)
  => {"sid":"[Filtered]","Max-Age":"3600","Path":"/"}

(Max-Age and Path are reported as if they're cookie key/value pairs.)

Not introduced here, but it probably should get fixed either now or in a cookie parsing consolidation follow-up issue.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created an issue for that: #24501

@s1gr1d
s1gr1d requested a review from isaacs September 18, 2026 11:47
@s1gr1d

s1gr1d commented Sep 18, 2026

Copy link
Copy Markdown
Member Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bf324ae. Configure here.


// An entry in `headersToSpanAttributes` does not exempt a header from the `dataCollection`
// filtering, so the allowlisted subset goes through the same pipeline as any other header.
Object.assign(spanAttributes, httpHeadersToSpanAttributes(allowlisted, client.getDataCollectionOptions()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoidable getClient in later callbacks

Low Severity

This is more an "is this necessary" check than a hard violation. The new getClient() calls run in later undici channel callbacks, where this file already notes the active context is no longer correct. That can yield no client or the wrong one, so headersToSpanAttributes is skipped or filtered with another client's dataCollection options. Flagged because the review rules ask to call out avoidable getClient() usage in production code.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit bf324ae. Configure here.

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.

Sensitive values bypass the denylist in header and cookie collection

3 participants