Skip to content

fix(core): Apply dataCollection.urlQueryParams to url.full and url.query - #23061

Open
chargome wants to merge 3 commits into
fix/filter-url-query-utilfrom
fix/gate-url-query-params-on-data-collection
Open

fix(core): Apply dataCollection.urlQueryParams to url.full and url.query#23061
chargome wants to merge 3 commits into
fix/filter-url-query-utilfrom
fix/gate-url-query-params-on-data-collection

Conversation

@chargome

@chargome chargome commented Aug 5, 2026

Copy link
Copy Markdown
Member

urlQueryParams only applied to request.query_string and requestDataIntegration. Everywhere else, query strings went to Sentry unfiltered — a ?token=… was sent as-is.

We filter spans in one central place (captureSpan) instead of at the ~57 write sites, which span ~18 packages and mostly have no access to the client. One place also means a new integration cannot leak by forgetting to filter.

Breadcrumbs do not go through the span pipeline, so those are filtered separately.

closes #23049

@chargome chargome self-assigned this Aug 5, 2026
@chargome

chargome commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

bugborzer run

Comment thread packages/core/test/lib/tracing/spans/captureSpan.test.ts
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 30.39 kB +0.91% +272 B 🔺
@sentry/browser - with treeshaking flags 28.58 kB +0.92% +260 B 🔺
@sentry/browser (incl. Tracing) 47.78 kB +0.52% +244 B 🔺
@sentry/browser (incl. Tracing + Span Streaming) 47.78 kB +0.52% +243 B 🔺
@sentry/browser (incl. Tracing, Profiling) 52.53 kB +0.47% +245 B 🔺
@sentry/browser (incl. Tracing, Replay) 87.17 kB +0.3% +254 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 76.57 kB +0.33% +247 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 91.84 kB +0.27% +245 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 104.5 kB +0.24% +249 B 🔺
@sentry/browser (incl. Feedback) 47.72 kB +0.59% +279 B 🔺
@sentry/browser (incl. sendFeedback) 35.22 kB +0.75% +261 B 🔺
@sentry/browser (incl. FeedbackAsync) 40.37 kB +0.67% +268 B 🔺
@sentry/browser (incl. Metrics) 31.45 kB +0.8% +249 B 🔺
@sentry/browser (incl. Logs) 31.68 kB +0.87% +272 B 🔺
@sentry/browser (incl. Metrics & Logs) 32.36 kB +0.79% +251 B 🔺
@sentry/react 32.15 kB +0.7% +221 B 🔺
@sentry/react (incl. Tracing) 50.03 kB +0.49% +241 B 🔺
@sentry/vue 35.48 kB +0.8% +281 B 🔺
@sentry/vue (incl. Tracing) 49.75 kB +0.48% +233 B 🔺
@sentry/svelte 30.41 kB +0.89% +266 B 🔺
CDN Bundle 32.4 kB +0.82% +263 B 🔺
CDN Bundle (incl. Tracing) 48.1 kB +0.62% +293 B 🔺
CDN Bundle (incl. Logs, Metrics) 33.92 kB +0.74% +247 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) 49.43 kB +0.51% +247 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 73.31 kB +0.4% +289 B 🔺
CDN Bundle (incl. Tracing, Replay) 85.69 kB +0.29% +247 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 87 kB +0.27% +233 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 91.53 kB +0.29% +261 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 92.83 kB +0.26% +235 B 🔺
CDN Bundle - uncompressed 95.96 kB +0.73% +690 B 🔺
CDN Bundle (incl. Tracing) - uncompressed 143.46 kB +0.48% +681 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 100.59 kB +0.7% +690 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 147.44 kB +0.47% +681 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 225.29 kB +0.31% +690 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed 262.71 kB +0.26% +681 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 266.68 kB +0.26% +681 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 276.42 kB +0.25% +681 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 280.37 kB +0.25% +681 B 🔺
@sentry/nextjs (client) 52.58 kB +0.43% +220 B 🔺
@sentry/sveltekit (client) 48.26 kB +0.56% +266 B 🔺
@sentry/core/server 65.81 kB +0.42% +270 B 🔺
@sentry/core/browser 52.04 kB +0.42% +213 B 🔺
@sentry/node 120.7 kB +0.22% +256 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 0 B added added
@sentry/node - without tracing 84.32 kB +0.28% +232 B 🔺
@sentry/aws-serverless 92.8 kB +0.22% +203 B 🔺
@sentry/cloudflare (withSentry) - minified 218.36 kB +0.26% +551 B 🔺
@sentry/cloudflare (withSentry) 538.74 kB +0.32% +1.68 kB 🔺

View base workflow run

@chargome

chargome commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

bugbot 📿

@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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit b82b19e. Configure here.

chargome and others added 2 commits August 5, 2026 16:42
…rl.query`

`urlQueryParams` only applied to `request.query_string` and
`requestDataIntegration`. Everywhere else, query strings went to Sentry
unfiltered.

Spans are filtered in one central place (`captureSpan`) instead of at the ~57
write sites, which span ~18 packages and mostly have no access to the client.
The pass runs after the `processSpan` hooks so integration-set attributes are
covered, and before `beforeSendSpan` since explicitly user-attached data is not
gated by `dataCollection`.

Breadcrumbs do not go through the span pipeline, so those are filtered
separately at write time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Breadcrumb query filtering had no test coverage, so re-leaking a token would not
have failed CI. Adds cases for the default denylist, off mode, allowList and
extra deny terms on both outgoing request breadcrumb paths. The node fetch path
needs its own file because the existing test module mocks `getClient` without
`getDataCollectionOptions`.

Also fixes two span tests that claimed more than they asserted: one checks the
span name is untouched but never looked at it, and the other claimed to cover
attributes set after the span starts while passing them in at creation. The
latter now registers a `processSpan` subscriber, mirroring how
`requestDataIntegration` sets `url.full`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chargome
chargome force-pushed the fix/gate-url-query-params-on-data-collection branch from b82b19e to 0e39522 Compare August 5, 2026 14:42
@chargome
chargome marked this pull request as ready for review August 5, 2026 15:02
@chargome
chargome requested a review from a team as a code owner August 5, 2026 15:02
@chargome
chargome requested review from andreiborza, msonnb, nicohrubec and stephanie-anderson and removed request for a team and stephanie-anderson August 5, 2026 15:02

@nicohrubec nicohrubec 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.

lgtm, costs some bundle size but I think it's worth it given the blast radius

@chargome

chargome commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

@nicohrubec yeah this was the tradeoff to adding it centrally to captureSpan

Comment thread packages/core/src/utils/data-collection/filterUrlSpanAttributes.ts Outdated
Comment thread packages/core/src/utils/data-collection/filterUrlSpanAttributes.ts
Comment on lines +79 to +80
// Runs after the hooks above so that URL attributes set by integrations are filtered too
filterUrlSpanAttributes(spanJSON, client.getDataCollectionOptions().urlQueryParams);

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/q: Is it fine to apply this here where we can no longer distinguish between user-set vs. SDK-set values? My understanding was dataCollection is only scoped to SDK-set values? If I'm wrong, feel free to ignore this. Otherwise, we probably need to move filtering to the sites where we set the url attributes.

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.

That is the tradeoff: we have about ~50 callsites that would need to be guarded, new integrations would easily miss this filtering step that's why I put it here

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'll try going down the helper function route we discussed offline

Comment on lines +21 to +29
export function filterUrlSpanAttributes(spanJSON: StreamedSpanJSON, behavior: CollectBehavior): void {
const attributes = spanJSON.attributes;
if (!attributes) {
return;
}

mapStringAttribute(attributes, URL_FULL, value => filterUrlQuery(value, behavior));
mapStringAttribute(attributes, URL_QUERY, value => filterQueryParams(value, behavior));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Static-lifecycle spans bypass the new url.full/url.query query-param filtering

This filter only runs inside captureSpan, so spans serialized through the static trace lifecycle — transaction-embedded spans in _convertSpanToTransaction (tracing/sentrySpan.ts) and static standalone spans via captureStandaloneSpanWithStaticCallback (tracing/spans/captureSpan.ts:184) — still send url.full/url.query query strings such as ?token=… to Sentry unfiltered.

Evidence
  • filterUrlSpanAttributes has exactly one call site: captureSpan.ts:80 inside captureSpan, which only serves the streaming span pipeline.
  • captureStandaloneSpanWithStaticCallback (captureSpan.ts:184-207) serializes via spanToJSON and returns without calling the filter; it is reached from sendStandaloneSpan when traceLifecycle === 'static' with a static beforeSendSpan (sentrySpan.ts:567).
  • _convertSpanToTransaction (sentrySpan.ts:461-471) embeds child spans into transaction events via spanToJSON(descendant) with no filtering — the standard span path when traceLifecycle: 'static' is configured.
  • Integrations set these attributes regardless of lifecycle: browser/src/tracing/request.ts:398 and core/src/fetch.ts:403 set URL_FULL/URL_QUERY from request URLs, so even the default sensitive-key denylist in shouldFilterDataKey never runs for these spans.
  • client.ts:242 normalizes traceLifecycle to 'stream' unless explicitly 'static', so the gap only affects the opt-in deprecated static lifecycle — hence low severity.

Identified by Warden · security-review · DUQ-S9Q

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.

Audit URL data collection against the dataCollection spec (urlQueryParams)

3 participants