Skip to content

Commit 6ce620e

Browse files
authored
fix(core): Always redact content of sensitive headers regardless of sendDefaultPii (#18311)
In case an HTTP header is considered "sensitive" (could contain tokens), the value is already filtered within the SDK. --- Follow-up on this PR: - #17475
1 parent 235c865 commit 6ce620e

File tree

10 files changed

+68
-93
lines changed

10 files changed

+68
-93
lines changed

packages/astro/src/server/middleware.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,7 @@ async function instrumentRequestStartHttpServerSpan(
219219
// This is here for backwards compatibility, we used to set this here before
220220
method,
221221
url: stripUrlQueryAndFragment(ctx.url.href),
222-
...httpHeadersToSpanAttributes(
223-
winterCGHeadersToDict(request.headers),
224-
getClient()?.getOptions().sendDefaultPii ?? false,
225-
),
222+
...httpHeadersToSpanAttributes(winterCGHeadersToDict(request.headers)),
226223
};
227224

228225
if (parametrizedRoute) {

packages/bun/src/integrations/bunserver.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
captureException,
44
continueTrace,
55
defineIntegration,
6-
getClient,
76
httpHeadersToSpanAttributes,
87
isURLObjectRelative,
98
parseStringToURLObject,
@@ -207,9 +206,7 @@ function wrapRequestHandler<T extends RouteHandler = RouteHandler>(
207206
routeName = route;
208207
}
209208

210-
const client = getClient();
211-
const sendDefaultPii = client?.getOptions().sendDefaultPii ?? false;
212-
Object.assign(attributes, httpHeadersToSpanAttributes(request.headers.toJSON(), sendDefaultPii));
209+
Object.assign(attributes, httpHeadersToSpanAttributes(request.headers.toJSON()));
213210

214211
isolationScope.setSDKProcessingMetadata({
215212
normalizedRequest: {

packages/cloudflare/src/request.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,7 @@ export function wrapRequestHandler(
6666
attributes['user_agent.original'] = userAgentHeader;
6767
}
6868

69-
const sendDefaultPii = options.sendDefaultPii ?? false;
70-
Object.assign(attributes, httpHeadersToSpanAttributes(winterCGHeadersToDict(request.headers), sendDefaultPii));
69+
Object.assign(attributes, httpHeadersToSpanAttributes(winterCGHeadersToDict(request.headers)));
7170

7271
attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] = 'http.server';
7372

packages/core/src/utils/request.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,19 @@ function getAbsoluteUrl({
129129
}
130130

131131
// "-user" because otherwise it would match "user-agent"
132-
const SENSITIVE_HEADER_SNIPPETS = ['auth', 'token', 'secret', 'cookie', '-user', 'password', 'key'];
132+
const SENSITIVE_HEADER_SNIPPETS = [
133+
'auth',
134+
'token',
135+
'secret',
136+
'cookie',
137+
'-user',
138+
'password',
139+
'key',
140+
'jwt',
141+
'bearer',
142+
'sso',
143+
'saml',
144+
];
133145

134146
/**
135147
* Converts incoming HTTP request headers to OpenTelemetry span attributes following semantic conventions.
@@ -140,26 +152,25 @@ const SENSITIVE_HEADER_SNIPPETS = ['auth', 'token', 'secret', 'cookie', '-user',
140152
*/
141153
export function httpHeadersToSpanAttributes(
142154
headers: Record<string, string | string[] | undefined>,
143-
sendDefaultPii: boolean = false,
144155
): Record<string, string> {
145156
const spanAttributes: Record<string, string> = {};
146157

147158
try {
148159
Object.entries(headers).forEach(([key, value]) => {
149-
if (value !== undefined) {
150-
const lowerCasedKey = key.toLowerCase();
151-
152-
if (!sendDefaultPii && SENSITIVE_HEADER_SNIPPETS.some(snippet => lowerCasedKey.includes(snippet))) {
153-
return;
154-
}
160+
if (value == null) {
161+
return;
162+
}
155163

156-
const normalizedKey = `http.request.header.${lowerCasedKey.replace(/-/g, '_')}`;
164+
const lowerCasedKey = key.toLowerCase();
165+
const isSensitive = SENSITIVE_HEADER_SNIPPETS.some(snippet => lowerCasedKey.includes(snippet));
166+
const normalizedKey = `http.request.header.${lowerCasedKey.replace(/-/g, '_')}`;
157167

158-
if (Array.isArray(value)) {
159-
spanAttributes[normalizedKey] = value.map(v => (v !== null && v !== undefined ? String(v) : v)).join(';');
160-
} else if (typeof value === 'string') {
161-
spanAttributes[normalizedKey] = value;
162-
}
168+
if (isSensitive) {
169+
spanAttributes[normalizedKey] = '[Filtered]';
170+
} else if (Array.isArray(value)) {
171+
spanAttributes[normalizedKey] = value.map(v => (v != null ? String(v) : v)).join(';');
172+
} else if (typeof value === 'string') {
173+
spanAttributes[normalizedKey] = value;
163174
}
164175
});
165176
} catch {

packages/core/test/lib/utils/request.test.ts

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -613,61 +613,25 @@ describe('request utils', () => {
613613
});
614614

615615
describe('PII filtering', () => {
616-
it('filters out sensitive headers when sendDefaultPii is false (default)', () => {
617-
const headers = {
618-
'Content-Type': 'application/json',
619-
'User-Agent': 'test-agent',
620-
Authorization: 'Bearer secret-token',
621-
Cookie: 'session=abc123',
622-
'X-API-Key': 'api-key-123',
623-
'X-Auth-Token': 'auth-token-456',
624-
};
625-
626-
const result = httpHeadersToSpanAttributes(headers, false);
627-
628-
expect(result).toEqual({
629-
'http.request.header.content_type': 'application/json',
630-
'http.request.header.user_agent': 'test-agent',
631-
// Sensitive headers should be filtered out
632-
});
633-
});
634-
635-
it('includes sensitive headers when sendDefaultPii is true', () => {
636-
const headers = {
637-
'Content-Type': 'application/json',
638-
'User-Agent': 'test-agent',
639-
Authorization: 'Bearer secret-token',
640-
Cookie: 'session=abc123',
641-
'X-API-Key': 'api-key-123',
642-
};
643-
644-
const result = httpHeadersToSpanAttributes(headers, true);
645-
646-
expect(result).toEqual({
647-
'http.request.header.content_type': 'application/json',
648-
'http.request.header.user_agent': 'test-agent',
649-
'http.request.header.authorization': 'Bearer secret-token',
650-
'http.request.header.cookie': 'session=abc123',
651-
'http.request.header.x_api_key': 'api-key-123',
652-
});
653-
});
654-
655616
it('filters sensitive headers case-insensitively', () => {
656617
const headers = {
657618
AUTHORIZATION: 'Bearer secret-token',
658619
Cookie: 'session=abc123',
659-
'x-api-key': 'key-123',
620+
'x-aPi-kEy': 'key-123',
660621
'Content-Type': 'application/json',
661622
};
662623

663-
const result = httpHeadersToSpanAttributes(headers, false);
624+
const result = httpHeadersToSpanAttributes(headers);
664625

665626
expect(result).toEqual({
666627
'http.request.header.content_type': 'application/json',
628+
'http.request.header.cookie': '[Filtered]',
629+
'http.request.header.x_api_key': '[Filtered]',
630+
'http.request.header.authorization': '[Filtered]',
667631
});
668632
});
669633

670-
it('filters comprehensive list of sensitive headers', () => {
634+
it('always filters comprehensive list of sensitive headers', () => {
671635
const headers = {
672636
'Content-Type': 'application/json',
673637
'User-Agent': 'test-agent',
@@ -692,15 +656,41 @@ describe('request utils', () => {
692656
'X-Private-Key': 'private',
693657
'X-Forwarded-user': 'user',
694658
'X-Forwarded-authorization': 'auth',
659+
'x-jwt-token': 'jwt',
660+
'x-bearer-token': 'bearer',
661+
'x-sso-token': 'sso',
662+
'x-saml-token': 'saml',
695663
};
696664

697-
const result = httpHeadersToSpanAttributes(headers, false);
665+
const result = httpHeadersToSpanAttributes(headers);
698666

667+
// Sensitive headers are always included and redacted
699668
expect(result).toEqual({
700669
'http.request.header.content_type': 'application/json',
701670
'http.request.header.user_agent': 'test-agent',
702671
'http.request.header.accept': 'application/json',
703672
'http.request.header.host': 'example.com',
673+
'http.request.header.authorization': '[Filtered]',
674+
'http.request.header.cookie': '[Filtered]',
675+
'http.request.header.set_cookie': '[Filtered]',
676+
'http.request.header.x_api_key': '[Filtered]',
677+
'http.request.header.x_auth_token': '[Filtered]',
678+
'http.request.header.x_secret': '[Filtered]',
679+
'http.request.header.x_secret_key': '[Filtered]',
680+
'http.request.header.www_authenticate': '[Filtered]',
681+
'http.request.header.proxy_authorization': '[Filtered]',
682+
'http.request.header.x_access_token': '[Filtered]',
683+
'http.request.header.x_csrf_token': '[Filtered]',
684+
'http.request.header.x_xsrf_token': '[Filtered]',
685+
'http.request.header.x_session_token': '[Filtered]',
686+
'http.request.header.x_password': '[Filtered]',
687+
'http.request.header.x_private_key': '[Filtered]',
688+
'http.request.header.x_forwarded_user': '[Filtered]',
689+
'http.request.header.x_forwarded_authorization': '[Filtered]',
690+
'http.request.header.x_jwt_token': '[Filtered]',
691+
'http.request.header.x_bearer_token': '[Filtered]',
692+
'http.request.header.x_sso_token': '[Filtered]',
693+
'http.request.header.x_saml_token': '[Filtered]',
704694
});
705695
});
706696
});

packages/nextjs/src/common/utils/addHeadersAsAttributes.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Span, WebFetchHeaders } from '@sentry/core';
2-
import { getClient, httpHeadersToSpanAttributes, winterCGHeadersToDict } from '@sentry/core';
2+
import { httpHeadersToSpanAttributes, winterCGHeadersToDict } from '@sentry/core';
33

44
/**
55
* Extracts HTTP request headers as span attributes and optionally applies them to a span.
@@ -12,15 +12,12 @@ export function addHeadersAsAttributes(
1212
return {};
1313
}
1414

15-
const client = getClient();
16-
const sendDefaultPii = client?.getOptions().sendDefaultPii ?? false;
17-
1815
const headersDict: Record<string, string | string[] | undefined> =
1916
headers instanceof Headers || (typeof headers === 'object' && 'get' in headers)
2017
? winterCGHeadersToDict(headers as Headers)
2118
: headers;
2219

23-
const headerAttributes = httpHeadersToSpanAttributes(headersDict, sendDefaultPii);
20+
const headerAttributes = httpHeadersToSpanAttributes(headersDict);
2421

2522
if (span) {
2623
span.setAttributes(headerAttributes);

packages/node-core/src/integrations/http/httpServerSpansIntegration.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions
136136
const method = normalizedRequest.method || request.method?.toUpperCase() || 'GET';
137137
const httpTargetWithoutQueryFragment = urlObj ? urlObj.pathname : stripUrlQueryAndFragment(fullUrl);
138138
const bestEffortTransactionName = `${method} ${httpTargetWithoutQueryFragment}`;
139-
const shouldSendDefaultPii = client.getOptions().sendDefaultPii ?? false;
140139

141140
// We use the plain tracer.startSpan here so we can pass the span kind
142141
const span = tracer.startSpan(bestEffortTransactionName, {
@@ -158,7 +157,7 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions
158157
'http.flavor': httpVersion,
159158
'net.transport': httpVersion?.toUpperCase() === 'QUIC' ? 'ip_udp' : 'ip_tcp',
160159
...getRequestContentLengthAttribute(request),
161-
...httpHeadersToSpanAttributes(normalizedRequest.headers || {}, shouldSendDefaultPii),
160+
...httpHeadersToSpanAttributes(normalizedRequest.headers || {}),
162161
},
163162
});
164163

packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
captureException,
44
debug,
55
flushIfServerless,
6-
getClient,
76
httpHeadersToSpanAttributes,
87
SEMANTIC_ATTRIBUTE_SENTRY_OP,
98
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
@@ -171,13 +170,9 @@ function getSpanAttributes(
171170
attributes['http.route'] = event.path;
172171
}
173172

174-
// Extract and add HTTP headers as span attributes
175-
const client = getClient();
176-
const sendDefaultPii = client?.getOptions().sendDefaultPii ?? false;
177-
178173
// Get headers from the Node.js request object
179174
const headers = event.node?.req?.headers || {};
180-
const headerAttributes = httpHeadersToSpanAttributes(headers, sendDefaultPii);
175+
const headerAttributes = httpHeadersToSpanAttributes(headers);
181176

182177
// Merge header attributes with existing attributes
183178
Object.assign(attributes, headerAttributes);

packages/remix/src/server/instrumentServer.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -310,10 +310,7 @@ function wrapRequestHandler<T extends ServerBuild | (() => ServerBuild | Promise
310310
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
311311
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
312312
method: request.method,
313-
...httpHeadersToSpanAttributes(
314-
winterCGHeadersToDict(request.headers),
315-
clientOptions.sendDefaultPii ?? false,
316-
),
313+
...httpHeadersToSpanAttributes(winterCGHeadersToDict(request.headers)),
317314
},
318315
},
319316
async span => {

packages/sveltekit/src/server-common/handle.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
continueTrace,
44
debug,
55
flushIfServerless,
6-
getClient,
76
getCurrentScope,
87
getDefaultIsolationScope,
98
getIsolationScope,
@@ -179,10 +178,7 @@ async function instrumentHandle(
179178
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
180179
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeName ? 'route' : 'url',
181180
'sveltekit.tracing.original_name': originalName,
182-
...httpHeadersToSpanAttributes(
183-
winterCGHeadersToDict(event.request.headers),
184-
getClient()?.getOptions().sendDefaultPii ?? false,
185-
),
181+
...httpHeadersToSpanAttributes(winterCGHeadersToDict(event.request.headers)),
186182
});
187183
}
188184

@@ -208,10 +204,7 @@ async function instrumentHandle(
208204
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
209205
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url',
210206
'http.method': event.request.method,
211-
...httpHeadersToSpanAttributes(
212-
winterCGHeadersToDict(event.request.headers),
213-
getClient()?.getOptions().sendDefaultPii ?? false,
214-
),
207+
...httpHeadersToSpanAttributes(winterCGHeadersToDict(event.request.headers)),
215208
},
216209
name: routeName,
217210
},

0 commit comments

Comments
 (0)