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
99 changes: 82 additions & 17 deletions packages/core/src/utils/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,44 @@ const SENSITIVE_HEADER_SNIPPETS = [
'cookie',
];

/**
* Extra substrings matched only against individual Cookie / Set-Cookie **names** (not header names),
* so we can cover common session secrets that do not match {@link SENSITIVE_HEADER_SNIPPETS}
* (e.g. `connect.sid` does not contain `session`) without false positives on arbitrary HTTP headers.
*
* Cookie names are checked with the same `includes()` list as headers plus these entries; omit redundant
* cookie-only snippets that are already implied by a header match (e.g. `oauth` → `auth`, `id_token` → `token`,
* `next-auth` → `auth`).
*/
const SENSITIVE_COOKIE_NAME_SNIPPETS = [
// Express / Connect default session cookie
'.sid',
// Opaque session ids (PHPSESSID, ASPSESSIONID*, BIGipServer*, *sessid*, …)
'sessid',
// Laravel etc. "remember me" tokens
'remember',
// OIDC / OAuth auxiliary (`oauth*` covered by header snippet `auth`)
'oidc',
'pkce',
'nonce',
// RFC 6265bis high-security cookie name prefixes
'__secure-',
'__host-',
// Load balancer / CDN sticky-session cookies (opaque routing tokens)
'awsalb',
'awselb',
'akamai',
// BaaS / IdP session cookies (names often omit "session")
'__stripe',
'cognito',
'firebase',
'supabase',
'sb-',
// Step-up / MFA cookies
'mfa',
'2fa',
];

const PII_HEADER_SNIPPETS = ['x-forwarded-', '-user'];

/**
Expand Down Expand Up @@ -196,17 +234,23 @@ export function httpHeadersToSpanAttributes(

const lowerCasedCookieKey = cookieKey.toLowerCase();

addSpanAttribute(
addSpanAttribute({
spanAttributes,
lowerCasedHeaderKey,
lowerCasedCookieKey,
cookieValue,
headerKey: lowerCasedHeaderKey,
cookieKey: lowerCasedCookieKey,
value: cookieValue,
sendDefaultPii,
lifecycle,
);
});
}
} else {
addSpanAttribute(spanAttributes, lowerCasedHeaderKey, '', value, sendDefaultPii, lifecycle);
addSpanAttribute({
spanAttributes,
headerKey: lowerCasedHeaderKey,
value,
sendDefaultPii,
lifecycle,
});
}
});
} catch {
Expand All @@ -220,15 +264,31 @@ function normalizeAttributeKey(key: string): string {
return key.replace(/-/g, '_');
}

function addSpanAttribute(
spanAttributes: Record<string, string>,
headerKey: string,
cookieKey: string,
value: string | string[] | undefined,
sendPii: boolean,
lifecycle: 'request' | 'response',
): void {
const headerValue = handleHttpHeader(cookieKey || headerKey, value, sendPii);
type AddSpanAttributeOptions = {
spanAttributes: Record<string, string>;
/** Lowercased HTTP header name (e.g. `cookie`, `set-cookie`, `accept`). */
headerKey: string;
/**
* Lowercased cookie name when this attribute comes from a parsed `Cookie` / `Set-Cookie` value.
* Omit for non-cookie headers; when present and non-empty, cookie-specific sensitivity rules apply.
*/
cookieKey?: string;
value: string | string[] | undefined;
sendDefaultPii: boolean;
lifecycle: 'request' | 'response';
};

function addSpanAttribute({
spanAttributes,
headerKey,
cookieKey,
value,
sendDefaultPii,
lifecycle,
}: AddSpanAttributeOptions): void {
const isCookieSubKey = Boolean(cookieKey);
const nameForSensitivity = cookieKey || headerKey;
const headerValue = handleHttpHeader(nameForSensitivity, value, sendDefaultPii, isCookieSubKey);
if (headerValue == null) {
return;
}
Expand All @@ -241,10 +301,15 @@ function handleHttpHeader(
lowerCasedKey: string,
value: string | string[] | undefined,
sendPii: boolean,
isCookieSubKey: boolean = false,
): string | undefined {
const snippetsForSensitivity = isCookieSubKey
? [...SENSITIVE_HEADER_SNIPPETS, ...SENSITIVE_COOKIE_NAME_SNIPPETS]
: SENSITIVE_HEADER_SNIPPETS;

const isSensitive = sendPii
? SENSITIVE_HEADER_SNIPPETS.some(snippet => lowerCasedKey.includes(snippet))
: [...PII_HEADER_SNIPPETS, ...SENSITIVE_HEADER_SNIPPETS].some(snippet => lowerCasedKey.includes(snippet));
? snippetsForSensitivity.some(snippet => lowerCasedKey.includes(snippet))
: [...PII_HEADER_SNIPPETS, ...snippetsForSensitivity].some(snippet => lowerCasedKey.includes(snippet));

if (isSensitive) {
return '[Filtered]';
Expand Down
29 changes: 29 additions & 0 deletions packages/core/test/lib/utils/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,35 @@ describe('request utils', () => {
});
});

it('filters common framework and provider session-style cookie names', () => {
const headers = {
Cookie:
'connect.sid=s3cr3t; express.sid=opaque; PHPSESSID=abcd; theme=light; sb-access-token=x; __stripe_mid=y',
};

const result = httpHeadersToSpanAttributes(headers);

expect(result).toEqual({
'http.request.header.cookie.connect.sid': '[Filtered]',
'http.request.header.cookie.express.sid': '[Filtered]',
'http.request.header.cookie.phpsessid': '[Filtered]',
'http.request.header.cookie.theme': 'light',
'http.request.header.cookie.sb_access_token': '[Filtered]',
'http.request.header.cookie.__stripe_mid': '[Filtered]',
});
});

it('still filters session-style cookie names when sendDefaultPii is true', () => {
const headers = { Cookie: 'connect.sid=s3cr3t; analytics=1' };

const result = httpHeadersToSpanAttributes(headers, true);

expect(result).toEqual({
'http.request.header.cookie.connect.sid': '[Filtered]',
'http.request.header.cookie.analytics': '1',
});
});

it('adds a filtered cookie header when cookie header is present, but has no valid key=value pairs', () => {
const headers1 = { Cookie: ['key', 'val'] };
const result1 = httpHeadersToSpanAttributes(headers1);
Expand Down
Loading