-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
ref(core): Consolidate cookie parsing into one parser #24536
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4db0c17
3a6d020
6db8941
52c65f6
49f040a
741d721
34686ac
e339471
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,12 +7,12 @@ import type { Event } from '../types/event'; | |
| import type { IntegrationFn } from '../types/integration'; | ||
| import type { QueryParams, RequestEventData } from '../types/request'; | ||
| import type { StreamedSpanJSON } from '../types/span'; | ||
| import { parseCookie } from '../utils/cookie'; | ||
| import { cookiePairsToRecord, parseCookieHeader } from '../utils/cookie'; | ||
| import { SENSITIVE_COOKIE_NAME_SNIPPETS } from '../utils/data-collection/filtering-snippets'; | ||
| import { filterKeyValueData } from '../utils/data-collection/filterKeyValueData'; | ||
| import { filterQueryParams } from '../utils/data-collection/filterQueryParams'; | ||
| import { filterUrlQuery } from '../utils/data-collection/filterUrlQuery'; | ||
| import { httpHeadersToSpanAttributes } from '../utils/request'; | ||
| import { filterCookiePairs, httpHeadersToSpanAttributes } from '../utils/request'; | ||
| import { getUrlQuery } from '../utils/url'; | ||
| import { getClientIPAddress, ipHeaderNames } from '../vendor/getIpAddress'; | ||
| import { safeSetSpanJSONAttributes } from '../tracing/spans/captureSpan'; | ||
|
|
@@ -185,12 +185,20 @@ function addNormalizedRequestDataToSpan( | |
|
|
||
| // Process cookies before headers so normalizedRequest.cookies takes precedence | ||
| // over the raw cookie header (matching the processEvent path). | ||
| if (requestData.cookies && Object.keys(requestData.cookies).length > 0) { | ||
| const cookieString = Object.entries(requestData.cookies) | ||
| .map(([name, value]) => `${name}=${value}`) | ||
| .join('; '); | ||
| const cookieAttributes = httpHeadersToSpanAttributes({ cookie: cookieString }, dataCollection, 'request'); | ||
| safeSetSpanJSONAttributes(span, cookieAttributes); | ||
| if (include.cookies) { | ||
| // Cookies are not serialized to a string and re-parsed: a decoded value could contain ";" and | ||
| // split into a second, differently named cookie that escapes the denylist. | ||
| const cookieHeader = normalizedRequest.headers?.cookie; | ||
| const cookiePairs = normalizedRequest.cookies | ||
| ? Object.entries(normalizedRequest.cookies) | ||
| : cookieHeader | ||
| ? parseCookieHeader(cookieHeader, 'cookie') | ||
| : []; | ||
| if (cookiePairs.length > 0) { | ||
| safeSetSpanJSONAttributes(span, { | ||
| 'http.request.header.cookie': filterCookiePairs(cookiePairs, dataCollection.cookies), | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| if (requestData.headers) { | ||
|
|
@@ -245,7 +253,9 @@ function extractNormalizedRequestData( | |
| } | ||
|
|
||
| if (include.cookies) { | ||
| const cookies = normalizedRequest.cookies || (headers?.cookie ? parseCookie(headers.cookie) : undefined); | ||
| const cookies = | ||
| normalizedRequest.cookies || | ||
| (headers?.cookie ? cookiePairsToRecord(parseCookieHeader(headers.cookie, 'cookie')) : undefined); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a decoding-escape hole here. This bit produces a record of decoded values. Then line 190 turns that record back into a header string and line 192 re-parses the result. A percent-encoded Reproduced end to end through Two more lines keep this alive:
This is pre-existing. But since we're cleaning up cookie handling, and this is the last place that parses a cookie string it built itself, and the fix is small, probably a good idea to clean it up. Suggestion: only synthesize a cookie string when Or, maybe better: have
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. The span path now builds the attribute from cookie pairs ( |
||
| requestData.cookies = cookies || {}; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,31 +1,25 @@ | ||
| import type { CollectBehavior } from '../../types/datacollection'; | ||
| import { parseCookie } from '../cookie'; | ||
| import { FILTERED_VALUE as FILTERED, SENSITIVE_COOKIE_NAME_SNIPPETS } from './filtering-snippets'; | ||
| import { cookiePairsToRecord, parseCookieHeader } from '../cookie'; | ||
| import { SENSITIVE_COOKIE_NAME_SNIPPETS } from './filtering-snippets'; | ||
| import { filterKeyValueData } from './filterKeyValueData'; | ||
|
|
||
| /** | ||
| * Filters a cookie string according to a `CollectBehavior`. | ||
| * Filters a `Cookie` / `Set-Cookie` header string according to a `CollectBehavior`. | ||
| * | ||
| * When individual cookies can be parsed, each key-value pair is filtered | ||
| * independently. When parsing fails, the entire string is replaced with `[Filtered]`. | ||
| * A nameless segment inside an otherwise parseable string (`"opaque-blob; theme=dark"`) is | ||
| * dropped, since a record key cannot carry a `[Filtered]` marker without leaking the token. | ||
| * Each named cookie is filtered independently. A nameless cookie (`"opaque-blob"`, `"=opaque-blob"`) | ||
| * is reported as `{ '': '[Filtered]' }`, since its token is the value. | ||
| * | ||
| * @param headerName - `'set-cookie'` keeps only the cookie pair and ignores the attributes (`Path`, `Max-Age`, ...) | ||
| */ | ||
| export function filterCookies(cookieString: string, behavior: CollectBehavior): Record<string, string> | string { | ||
| export function filterCookies( | ||
| cookieString: string, | ||
| behavior: CollectBehavior, | ||
| headerName: 'cookie' | 'set-cookie', | ||
| ): Record<string, string> { | ||
| if (behavior === false) { | ||
| return {}; | ||
| } | ||
|
|
||
| try { | ||
| const parsed = parseCookie(cookieString); | ||
|
|
||
| // A non-empty string we cannot parse may still hold a session token, so it counts as sensitive. | ||
| if (Object.keys(parsed).length === 0) { | ||
| return cookieString ? FILTERED : {}; | ||
| } | ||
|
|
||
| return filterKeyValueData(parsed, behavior, SENSITIVE_COOKIE_NAME_SNIPPETS); | ||
| } catch { | ||
| return FILTERED; | ||
| } | ||
| const cookies = cookiePairsToRecord(parseCookieHeader(cookieString, headerName)); | ||
| return filterKeyValueData(cookies, behavior, SENSITIVE_COOKIE_NAME_SNIPPETS); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
q: were these tests just wrong before? As in, multiple cookies being set in one set-cookie header?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's the
Cookiesyntax. ForSet-Cookie, those other values are just other attributes like Max-Age or Path (which we don't anymore now - just key/value).But outcome of our offline discussion was that we might send the set-cookie attributes as well and see set-cookie as one joined string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update: we decided to keep only the key/value pair for
Set-Cookie. Attributes likePathorDomaincan hold PII so they stay out.