diff --git a/packages/core/src/utils/request.ts b/packages/core/src/utils/request.ts index 6aaceb8fc201..3f7477e6459b 100644 --- a/packages/core/src/utils/request.ts +++ b/packages/core/src/utils/request.ts @@ -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']; /** @@ -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 { @@ -220,15 +264,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 +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]'; 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);