From 1a4bbd96cd8920acdd0c58215a9bdf9920743c88 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Fri, 24 Apr 2026 10:32:18 +0200 Subject: [PATCH 1/2] fix(core): Filter more cookie names for PII --- packages/core/src/utils/request.ts | 104 ++++++++++++++++--- packages/core/test/lib/utils/request.test.ts | 29 ++++++ 2 files changed, 116 insertions(+), 17 deletions(-) diff --git a/packages/core/src/utils/request.ts b/packages/core/src/utils/request.ts index 6aaceb8fc201..87f632bfe8e2 100644 --- a/packages/core/src/utils/request.ts +++ b/packages/core/src/utils/request.ts @@ -149,6 +149,49 @@ 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. + */ +const SENSITIVE_COOKIE_NAME_SNIPPETS = [ + // Express / Connect default session cookie + '.sid', + // PHP session cookie + 'phpsess', + // Common opaque session id suffix / cookie names (e.g. ASPSESSIONID*, BIGipServer*) + 'sessid', + // Laravel etc. "remember me" tokens + 'remember', + // OAuth / OIDC auxiliary cookies + 'oauth', + 'oidc', + 'pkce', + 'nonce', + // Explicit token-style cookie names + 'id_token', + 'access_token', + 'refresh_token', + // RFC 6265bis cookie name prefixes for high-security cookies + '__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-', + // Auth.js / NextAuth.js + 'next-auth', + // Step-up / MFA cookies + 'mfa', + '2fa', +]; + const PII_HEADER_SNIPPETS = ['x-forwarded-', '-user']; /** @@ -196,17 +239,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 { @@ -220,15 +269,31 @@ function normalizeAttributeKey(key: string): string { return key.replace(/-/g, '_'); } -function addSpanAttribute( - spanAttributes: Record, - 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; + /** 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; } @@ -241,10 +306,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]'; diff --git a/packages/core/test/lib/utils/request.test.ts b/packages/core/test/lib/utils/request.test.ts index 73a19c2bfa45..250fcf8443c8 100644 --- a/packages/core/test/lib/utils/request.test.ts +++ b/packages/core/test/lib/utils/request.test.ts @@ -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); From 080ba13be5b1fde16ccfcd77f1ef4dc6ded638a1 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Fri, 24 Apr 2026 10:51:34 +0200 Subject: [PATCH 2/2] deduped --- packages/core/src/utils/request.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/core/src/utils/request.ts b/packages/core/src/utils/request.ts index 87f632bfe8e2..3f7477e6459b 100644 --- a/packages/core/src/utils/request.ts +++ b/packages/core/src/utils/request.ts @@ -153,26 +153,23 @@ const SENSITIVE_HEADER_SNIPPETS = [ * 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', - // PHP session cookie - 'phpsess', - // Common opaque session id suffix / cookie names (e.g. ASPSESSIONID*, BIGipServer*) + // Opaque session ids (PHPSESSID, ASPSESSIONID*, BIGipServer*, *sessid*, …) 'sessid', // Laravel etc. "remember me" tokens 'remember', - // OAuth / OIDC auxiliary cookies - 'oauth', + // OIDC / OAuth auxiliary (`oauth*` covered by header snippet `auth`) 'oidc', 'pkce', 'nonce', - // Explicit token-style cookie names - 'id_token', - 'access_token', - 'refresh_token', - // RFC 6265bis cookie name prefixes for high-security cookies + // RFC 6265bis high-security cookie name prefixes '__secure-', '__host-', // Load balancer / CDN sticky-session cookies (opaque routing tokens) @@ -185,8 +182,6 @@ const SENSITIVE_COOKIE_NAME_SNIPPETS = [ 'firebase', 'supabase', 'sb-', - // Auth.js / NextAuth.js - 'next-auth', // Step-up / MFA cookies 'mfa', '2fa',