Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions packages/astro/src/server/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,7 @@ async function instrumentRequestStartHttpServerSpan(
// This is here for backwards compatibility, we used to set this here before
method,
url: stripUrlQueryAndFragment(ctx.url.href),
...httpHeadersToSpanAttributes(
winterCGHeadersToDict(request.headers),
getClient()?.getOptions().sendDefaultPii ?? false,
),
...httpHeadersToSpanAttributes(winterCGHeadersToDict(request.headers)),
};

if (parametrizedRoute) {
Expand Down
5 changes: 1 addition & 4 deletions packages/bun/src/integrations/bunserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
captureException,
continueTrace,
defineIntegration,
getClient,
httpHeadersToSpanAttributes,
isURLObjectRelative,
parseStringToURLObject,
Expand Down Expand Up @@ -207,9 +206,7 @@ function wrapRequestHandler<T extends RouteHandler = RouteHandler>(
routeName = route;
}

const client = getClient();
const sendDefaultPii = client?.getOptions().sendDefaultPii ?? false;
Object.assign(attributes, httpHeadersToSpanAttributes(request.headers.toJSON(), sendDefaultPii));
Object.assign(attributes, httpHeadersToSpanAttributes(request.headers.toJSON()));

isolationScope.setSDKProcessingMetadata({
normalizedRequest: {
Expand Down
3 changes: 1 addition & 2 deletions packages/cloudflare/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@ export function wrapRequestHandler(
attributes['user_agent.original'] = userAgentHeader;
}

const sendDefaultPii = options.sendDefaultPii ?? false;
Object.assign(attributes, httpHeadersToSpanAttributes(winterCGHeadersToDict(request.headers), sendDefaultPii));
Object.assign(attributes, httpHeadersToSpanAttributes(winterCGHeadersToDict(request.headers)));

attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] = 'http.server';

Expand Down
39 changes: 25 additions & 14 deletions packages/core/src/utils/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,19 @@ function getAbsoluteUrl({
}

// "-user" because otherwise it would match "user-agent"
const SENSITIVE_HEADER_SNIPPETS = ['auth', 'token', 'secret', 'cookie', '-user', 'password', 'key'];
const SENSITIVE_HEADER_SNIPPETS = [
'auth',
'token',
'secret',
'cookie',
'-user',
'password',
'key',
'jwt',
'bearer',
'sso',
'saml',
];

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

try {
Comment on lines 153 to 158

This comment was marked as resolved.

Object.entries(headers).forEach(([key, value]) => {
if (value !== undefined) {
const lowerCasedKey = key.toLowerCase();

if (!sendDefaultPii && SENSITIVE_HEADER_SNIPPETS.some(snippet => lowerCasedKey.includes(snippet))) {
return;
}
if (value == null) {
return;
}

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

if (Array.isArray(value)) {
spanAttributes[normalizedKey] = value.map(v => (v !== null && v !== undefined ? String(v) : v)).join(';');
} else if (typeof value === 'string') {
spanAttributes[normalizedKey] = value;
}
if (isSensitive) {
spanAttributes[normalizedKey] = '[Filtered]';
} else if (Array.isArray(value)) {
spanAttributes[normalizedKey] = value.map(v => (v != null ? String(v) : v)).join(';');
} else if (typeof value === 'string') {
spanAttributes[normalizedKey] = value;
}
});
} catch {
Expand Down
76 changes: 33 additions & 43 deletions packages/core/test/lib/utils/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,61 +613,25 @@ describe('request utils', () => {
});

describe('PII filtering', () => {
it('filters out sensitive headers when sendDefaultPii is false (default)', () => {
const headers = {
'Content-Type': 'application/json',
'User-Agent': 'test-agent',
Authorization: 'Bearer secret-token',
Cookie: 'session=abc123',
'X-API-Key': 'api-key-123',
'X-Auth-Token': 'auth-token-456',
};

const result = httpHeadersToSpanAttributes(headers, false);

expect(result).toEqual({
'http.request.header.content_type': 'application/json',
'http.request.header.user_agent': 'test-agent',
// Sensitive headers should be filtered out
});
});

it('includes sensitive headers when sendDefaultPii is true', () => {
const headers = {
'Content-Type': 'application/json',
'User-Agent': 'test-agent',
Authorization: 'Bearer secret-token',
Cookie: 'session=abc123',
'X-API-Key': 'api-key-123',
};

const result = httpHeadersToSpanAttributes(headers, true);

expect(result).toEqual({
'http.request.header.content_type': 'application/json',
'http.request.header.user_agent': 'test-agent',
'http.request.header.authorization': 'Bearer secret-token',
'http.request.header.cookie': 'session=abc123',
'http.request.header.x_api_key': 'api-key-123',
});
});

it('filters sensitive headers case-insensitively', () => {
const headers = {
AUTHORIZATION: 'Bearer secret-token',
Cookie: 'session=abc123',
'x-api-key': 'key-123',
'x-aPi-kEy': 'key-123',
'Content-Type': 'application/json',
};

const result = httpHeadersToSpanAttributes(headers, false);
const result = httpHeadersToSpanAttributes(headers);

expect(result).toEqual({
'http.request.header.content_type': 'application/json',
'http.request.header.cookie': '[Filtered]',
'http.request.header.x_api_key': '[Filtered]',
'http.request.header.authorization': '[Filtered]',
});
});

it('filters comprehensive list of sensitive headers', () => {
it('always filters comprehensive list of sensitive headers', () => {
const headers = {
'Content-Type': 'application/json',
'User-Agent': 'test-agent',
Expand All @@ -692,15 +656,41 @@ describe('request utils', () => {
'X-Private-Key': 'private',
'X-Forwarded-user': 'user',
'X-Forwarded-authorization': 'auth',
'x-jwt-token': 'jwt',
'x-bearer-token': 'bearer',
'x-sso-token': 'sso',
'x-saml-token': 'saml',
};

const result = httpHeadersToSpanAttributes(headers, false);
const result = httpHeadersToSpanAttributes(headers);

// Sensitive headers are always included and redacted
expect(result).toEqual({
'http.request.header.content_type': 'application/json',
'http.request.header.user_agent': 'test-agent',
'http.request.header.accept': 'application/json',
'http.request.header.host': 'example.com',
'http.request.header.authorization': '[Filtered]',
'http.request.header.cookie': '[Filtered]',
'http.request.header.set_cookie': '[Filtered]',
'http.request.header.x_api_key': '[Filtered]',
'http.request.header.x_auth_token': '[Filtered]',
'http.request.header.x_secret': '[Filtered]',
'http.request.header.x_secret_key': '[Filtered]',
'http.request.header.www_authenticate': '[Filtered]',
'http.request.header.proxy_authorization': '[Filtered]',
'http.request.header.x_access_token': '[Filtered]',
'http.request.header.x_csrf_token': '[Filtered]',
'http.request.header.x_xsrf_token': '[Filtered]',
'http.request.header.x_session_token': '[Filtered]',
'http.request.header.x_password': '[Filtered]',
'http.request.header.x_private_key': '[Filtered]',
'http.request.header.x_forwarded_user': '[Filtered]',
'http.request.header.x_forwarded_authorization': '[Filtered]',
'http.request.header.x_jwt_token': '[Filtered]',
'http.request.header.x_bearer_token': '[Filtered]',
'http.request.header.x_sso_token': '[Filtered]',
'http.request.header.x_saml_token': '[Filtered]',
});
});
});
Expand Down
7 changes: 2 additions & 5 deletions packages/nextjs/src/common/utils/addHeadersAsAttributes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Span, WebFetchHeaders } from '@sentry/core';
import { getClient, httpHeadersToSpanAttributes, winterCGHeadersToDict } from '@sentry/core';
import { httpHeadersToSpanAttributes, winterCGHeadersToDict } from '@sentry/core';

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

const client = getClient();
const sendDefaultPii = client?.getOptions().sendDefaultPii ?? false;

const headersDict: Record<string, string | string[] | undefined> =
headers instanceof Headers || (typeof headers === 'object' && 'get' in headers)
? winterCGHeadersToDict(headers as Headers)
: headers;

const headerAttributes = httpHeadersToSpanAttributes(headersDict, sendDefaultPii);
const headerAttributes = httpHeadersToSpanAttributes(headersDict);

if (span) {
span.setAttributes(headerAttributes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions
const method = normalizedRequest.method || request.method?.toUpperCase() || 'GET';
const httpTargetWithoutQueryFragment = urlObj ? urlObj.pathname : stripUrlQueryAndFragment(fullUrl);
const bestEffortTransactionName = `${method} ${httpTargetWithoutQueryFragment}`;
const shouldSendDefaultPii = client.getOptions().sendDefaultPii ?? false;

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

Expand Down
7 changes: 1 addition & 6 deletions packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
captureException,
debug,
flushIfServerless,
getClient,
httpHeadersToSpanAttributes,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
Expand Down Expand Up @@ -171,13 +170,9 @@ function getSpanAttributes(
attributes['http.route'] = event.path;
}

// Extract and add HTTP headers as span attributes
const client = getClient();
const sendDefaultPii = client?.getOptions().sendDefaultPii ?? false;

// Get headers from the Node.js request object
const headers = event.node?.req?.headers || {};
const headerAttributes = httpHeadersToSpanAttributes(headers, sendDefaultPii);
const headerAttributes = httpHeadersToSpanAttributes(headers);

// Merge header attributes with existing attributes
Object.assign(attributes, headerAttributes);
Expand Down
5 changes: 1 addition & 4 deletions packages/remix/src/server/instrumentServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,10 +310,7 @@ function wrapRequestHandler<T extends ServerBuild | (() => ServerBuild | Promise
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
method: request.method,
...httpHeadersToSpanAttributes(
winterCGHeadersToDict(request.headers),
clientOptions.sendDefaultPii ?? false,
),
...httpHeadersToSpanAttributes(winterCGHeadersToDict(request.headers)),
},
},
async span => {
Expand Down
11 changes: 2 additions & 9 deletions packages/sveltekit/src/server-common/handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
continueTrace,
debug,
flushIfServerless,
getClient,
getCurrentScope,
getDefaultIsolationScope,
getIsolationScope,
Expand Down Expand Up @@ -179,10 +178,7 @@ async function instrumentHandle(
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeName ? 'route' : 'url',
'sveltekit.tracing.original_name': originalName,
...httpHeadersToSpanAttributes(
winterCGHeadersToDict(event.request.headers),
getClient()?.getOptions().sendDefaultPii ?? false,
),
...httpHeadersToSpanAttributes(winterCGHeadersToDict(event.request.headers)),
});
}

Expand All @@ -208,10 +204,7 @@ async function instrumentHandle(
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url',
'http.method': event.request.method,
...httpHeadersToSpanAttributes(
winterCGHeadersToDict(event.request.headers),
getClient()?.getOptions().sendDefaultPii ?? false,
),
...httpHeadersToSpanAttributes(winterCGHeadersToDict(event.request.headers)),
},
name: routeName,
},
Expand Down