From 7958755f4f1efb4129d3f0073d7eb69da6ee5ccf Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Tue, 13 Oct 2026 09:29:12 +0200 Subject: [PATCH] fix(browser): Apply `urlQueryParams` to the Referer header `document.referrer` holds the full URL of the previous page, including its query string. `getHttpRequestData` now applies `dataCollection.urlQueryParams` to it, so sensitive query params no longer reach `request.headers.Referer` of error events and static transactions, or the `http.request.header.referer` span attribute. Co-Authored-By: Claude Opus 5.5 --- .../suites/integrations/httpContext/test.ts | 34 ++++++++++++++ .../test.ts | 3 +- packages/browser/src/helpers.ts | 10 +++-- .../browser/src/integrations/httpcontext.ts | 4 +- .../src/tracing/browserTracingIntegration.ts | 4 +- .../test/integrations/httpcontext.test.ts | 45 +++++++++++++++++++ 6 files changed, 92 insertions(+), 8 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpContext/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpContext/test.ts index e50a658b4c9a..40cce0c2ee28 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpContext/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/httpContext/test.ts @@ -37,6 +37,40 @@ sentryTest('httpContextIntegration filters sensitive query params in the URL', a expect(errorEvent.request?.url).toBe(`${url}?jwt=[Filtered]&page=1`); }); +sentryTest( + 'httpContextIntegration filters sensitive query params in the Referer header', + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const errorEventPromise = waitForErrorRequest(page); + + await page.goto(url, { referer: `${url}?jwt=sensitive-jwt-value&page=1` }); + + const errorEvent = envelopeRequestParser(await errorEventPromise); + + expect(errorEvent.request?.headers?.Referer).toBe(`${url}?jwt=[Filtered]&page=1`); + }, +); + +sentryTest( + 'httpContextIntegration filters sensitive query params in the referer span attribute', + async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + const url = await getLocalTestUrl({ testDir: __dirname }); + + const spanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + + await page.goto(url, { referer: `${url}?jwt=sensitive-jwt-value&page=1` }); + + const pageloadSpan = await spanPromise; + + expect(pageloadSpan.attributes['http.request.header.referer']).toEqual({ + type: 'array', + value: [`${url}?jwt=[Filtered]&page=1`], + }); + }, +); + sentryTest('httpContextIntegration captures url, user-agent, and referer', async ({ getLocalTestUrl, page }) => { sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-sensitive-query-params-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-sensitive-query-params-static/test.ts index 8bfc5a6df874..c20135d73286 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-sensitive-query-params-static/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-sensitive-query-params-static/test.ts @@ -16,10 +16,11 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === PAGELOAD); - await page.goto(`${url}?jwt=sensitive-jwt-value&page=1`); + await page.goto(`${url}?jwt=sensitive-jwt-value&page=1`, { referer: `${url}?jwt=sensitive-jwt-value&page=2` }); const eventData = envelopeRequestParser(await pageloadRequestPromise); expect(eventData.request?.url).toBe(`${url}?jwt=[Filtered]&page=1`); + expect(eventData.request?.headers?.Referer).toBe(`${url}?jwt=[Filtered]&page=2`); expect(eventData.spans?.find(({ op }) => op === BROWSER_REQUEST)?.description).toBe(`${url}?jwt=[Filtered]&page=1`); expect(eventData.spans?.find(({ op }) => op === RESOURCE_IMG)?.description).toBe( 'https://sentry-test-site.example/path/to/image.svg?token=[Filtered]&size=2', diff --git a/packages/browser/src/helpers.ts b/packages/browser/src/helpers.ts index 4b060abafc71..65255d987e7d 100644 --- a/packages/browser/src/helpers.ts +++ b/packages/browser/src/helpers.ts @@ -1,9 +1,10 @@ -import type { HandlerDataError, Mechanism, WrappedFunction } from '@sentry/core'; +import type { Client, HandlerDataError, Mechanism, WrappedFunction } from '@sentry/core'; import { addExceptionMechanism, addExceptionTypeValue, addNonEnumerableProperty, captureException, + filterCollectedUrl, getOriginalFunction, GLOBAL_OBJ, markFunctionWrapped, @@ -221,15 +222,18 @@ export function wrap( /** * Get HTTP request data from the current page. + * + * The `Referer` header has `dataCollection.urlQueryParams` of `client` applied. `url` is unfiltered, so + * callers must pass it through `filterCollectedUrl`. */ -export function getHttpRequestData(): { url: string; headers: Record } { +export function getHttpRequestData(client: Client): { url: string; headers: Record } { // grab as much info as exists and add it to the event const url = getLocationHref(); const { referrer } = WINDOW.document || {}; const { userAgent } = WINDOW.navigator || {}; const headers = { - ...(referrer && { Referer: referrer }), + ...(referrer && { Referer: filterCollectedUrl(referrer, client) }), ...(userAgent && { 'User-Agent': userAgent }), }; const request = { diff --git a/packages/browser/src/integrations/httpcontext.ts b/packages/browser/src/integrations/httpcontext.ts index 45961e307b15..1f20ffb9e843 100644 --- a/packages/browser/src/integrations/httpcontext.ts +++ b/packages/browser/src/integrations/httpcontext.ts @@ -22,7 +22,7 @@ export const httpContextIntegration = defineIntegration(() => { return; } - const { url, headers: collectedHeaders } = getHttpRequestData(); + const { url, headers: collectedHeaders } = getHttpRequestData(client); // We only filter the headers we collected ourselves, so headers the user set are left alone. Going by key // means we also catch these headers when `browserTracingIntegration` already put them on the event. @@ -47,7 +47,7 @@ export const httpContextIntegration = defineIntegration(() => { return; } - const reqData = getHttpRequestData(); + const reqData = getHttpRequestData(client); // `httpHeadersToSpanAttributes` would also work here, but its cookie and array handling never runs for these // two headers and costs every browser bundle ~400B gzip. diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index a900309bf992..b02a5d5ebcca 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -574,7 +574,7 @@ export const browserTracingIntegration = ((options: Partial { expect(event.request?.url).toBe('https://example.com/?jwt=[Filtered]&page=1'); }); + it('filters sensitive query params in the Referer header attached to events', () => { + const client = new BrowserClient(getDefaultBrowserClientOptions()); + const event: Event = {}; + + document.referrer = 'https://example.com/?jwt=baz&page=1'; + try { + httpContextIntegration().preprocessEvent!(event, {}, client); + } finally { + document.referrer = 'https://example.com'; + } + + expect(event.request?.headers?.Referer).toBe('https://example.com/?jwt=[Filtered]&page=1'); + }); + + it('removes the query from the Referer header attached to events when `urlQueryParams` is disabled', () => { + const client = new BrowserClient(getDefaultBrowserClientOptions({ dataCollection: { urlQueryParams: false } })); + const event: Event = {}; + + document.referrer = 'https://example.com/?jwt=baz&page=1'; + try { + httpContextIntegration().preprocessEvent!(event, {}, client); + } finally { + document.referrer = 'https://example.com'; + } + + expect(event.request?.headers?.Referer).toBe('https://example.com/'); + }); + + it('filters sensitive query params in the referer attribute of segment spans', () => { + const client = new BrowserClient(getDefaultBrowserClientOptions()); + const span: Partial = { + is_segment: true, + attributes: { [SENTRY_OP]: 'pageload' }, + }; + + document.referrer = 'https://example.com/?jwt=baz&page=1'; + try { + httpContextIntegration().processSpan!(span as StreamedSpanJSON, client); + } finally { + document.referrer = 'https://example.com'; + } + + expect(span.attributes?.['http.request.header.referer']).toEqual(['https://example.com/?jwt=[Filtered]&page=1']); + }); + it('leaves a user-supplied event URL untouched', () => { const client = new BrowserClient(getDefaultBrowserClientOptions()); const event: Event = { request: { url: 'https://example.com/?jwt=user-set' } };