Repository navigation
fix: Filter sensitive cookie values instead of sending raw cookies - #5894
jamescrosswell wants to merge 8 commits into
Conversation
Parse Cookie and Set-Cookie values per DataCollection spec 0.15.0 and filter each cookie by name, so raw cookie headers no longer reach request.cookies. Server transactions also carry the http.request.header.cookie string-array attribute. Fixes #5423 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5894 +/- ##
==========================================
+ Coverage 76.87% 76.95% +0.07%
==========================================
Files 533 535 +2
Lines 19847 19918 +71
Branches 3920 3950 +30
==========================================
+ Hits 15258 15327 +69
- Misses 3709 3710 +1
- Partials 880 881 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A malformed pair now becomes a single [Filtered] element instead of discarding the other cookies in the header, matching sentry-java. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rse-cookies-196ba8 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading cookies by name returned the first match for each duplicate, so a repeated cookie name sent the first value twice. Index the collection instead, which also drops the LINQ closure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| return SensitiveDataFilter.FilteredValue; | ||
| } | ||
|
|
||
| return $"{name}={SensitiveDataFilter.FilterValue(name, value, behavior, SensitiveDataFilter.SensitiveCookieNameTerms)}"; |
There was a problem hiding this comment.
The default auth cookies from ASP.NET Core and OWIN don't match any term, so their values still go out as-is. .AspNetCore.Cookies is the ticket AddCookie() writes, and .AspNet.Cookies and .AspNet.ApplicationCookie are the OWIN and ASP.NET Identity 2 defaults. Anyone with one of those values can sign in as that user until it expires. That isn't new, since the whole header went out before, but it's the cookie I'd most expect this PR to catch. .AspNetCore.Identity.Application and .AspNetCore.Session are fine, since identity and session already match them.
What do you think about adding the framework prefixes to SensitiveCookieNameTerms in SensitiveDataFilter.cs? I can't suggest on that file here because it isn't in the diff.
"mfa",
"2fa",
".aspnetcore.",
".aspnet.",
];That also covers the chunked .AspNetCore.CookiesC1 cookies, the antiforgery cookie and custom schemes such as .AspNetCore.Admin. The downside is .AspNetCore.Culture. Its value gets hidden too, and once the cookie options are wired up, an allow list can't bring it back because cookie terms win over it. Exact names would avoid that but miss the custom schemes, so I'd still go with the prefixes. Adding .AspNetCore.Cookies and .AspNet.ApplicationCookie to the FilterCookieHeader_SensitiveName_FiltersValueAndKeepsName cases would pin it.
These names only mean something to .NET, so I'd add them here. sentry-java does something similar. It filters JSESSIONID and a few others by exact name, and its Spring integration adds the server's session cookie name at runtime. The spec doesn't ask SDKs for this, so I can raise it separately in sentry-docs.
|
|
||
| transaction?.SetExtra(OtelSemanticConventions.AttributeHttpRequestMethod, context.Request.Method); | ||
|
|
||
| if (_options.SendDefaultPii && transaction is not null) |
There was a problem hiding this comment.
This runs on every request when SendDefaultPii is on, even when the transaction isn't sampled or tracing is off. SetData on an unsampled transaction drops the value. With a 1.1 KB browser cookie header, I measured about 10 KB of extra allocations per request with tracing off. Should we only parse for sampled transactions?
| if (_options.SendDefaultPii && transaction is not null) | |
| if (_options.SendDefaultPii && transaction is { IsSampled: true }) |
StartSentryTransaction in Sentry.AspNet could take the same check. It already built the cookie string for unsampled transactions before this PR, so that one matters less.
| return filtered.ToArray(); | ||
| } | ||
|
|
||
| internal static string ToEventCookies(IEnumerable<string> filteredCookies) => |
There was a problem hiding this comment.
About the Relay question in the description, Relay does keep the other cookies. It also records an invalid_data error on request.cookies, though. Sentry doesn't hide that error type, so the issue page shows a "Processing Error" for the event. Nameless cookies already did that with the raw header. What's new is every cookie we now replace because of its characters, such as a value with a space, a comma or raw JSON, or a name with : or [. Relay's parser accepted those before and now gets a bare [Filtered].
If we'd rather avoid that, we could drop the bare elements from the event string only. The span attribute would keep the spec's marker.
internal static string ToEventCookies(IEnumerable<string> filteredCookies) =>
string.Join(EventCookieSeparator, filteredCookies.Where(cookie => cookie != SensitiveDataFilter.FilteredValue));That changes the expected strings in five tests. Leaving it as is works for me too, since sentry-java also sends a bare [Filtered].
| internal static string[] FilterCookieHeaders(IEnumerable<string?> headers, KeyValueCollectionBehavior? behavior = null) => | ||
| headers.SelectMany(header => FilterCookieHeader(header, behavior)).ToArray(); | ||
|
|
||
| internal static string[] FilterSetCookieHeader(string? header, KeyValueCollectionBehavior? behavior = null) |
There was a problem hiding this comment.
Since nothing calls this yet, what do you think about leaving it and its seven tests out until we collect Set-Cookie? The follow-up can add it together with its first caller.
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("theme=dark, user_session=abc")] |
There was a problem hiding this comment.
Every comma case here also has a space, so the space check catches them, and nothing tests the comma on its own. If someone allowed commas in IsCookieOctet, the session value in theme=dark,user_session=abc would go out inside theme's value, and all the tests would still pass.
| [InlineData("theme=dark, user_session=abc")] | |
| [InlineData("theme=dark, user_session=abc")] | |
| [InlineData("theme=dark,user_session=abc")] |
| } | ||
|
|
||
| [Fact] | ||
| public void FilterCookieHeader_MultiplePairs_NeverContainsSensitiveValue() |
There was a problem hiding this comment.
Any realistic break I tried here also fails another test, since the other examples already assert these values. The same goes for FilterCookieHeader_NoSpaceAfterSemicolon_SplitsCookies. It only failed together with FilterCookieHeader_IssueExample_FiltersEachCookie, which also has a cookie with no space after the semicolon. Could we drop these two?
| } | ||
|
|
||
| [Fact] | ||
| public void StartSentryTransaction_SendDefaultPii_set_to_true_filters_cookies() |
There was a problem hiding this comment.
This covers everything that StartSentryTransaction_SendDefaultPii_set_to_true_sets_cookies above checks, since theme=dark-mode goes through the same path as foo=bar. Could we merge the two into one test?
…actions Adds the .AspNetCore. and .AspNet. prefixes to the sensitive cookie name terms, so default auth, antiforgery and custom-scheme cookies are filtered. Cookie parsing now only runs for sampled transactions. Trims redundant tests and adds a comma-only malformed case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HTTP/2 sends header names in lowercase, so the case-sensitive match skipped Request.Cookies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 62dd4f9. Configure here.
…s off The case-sensitive skip let a lowercase cookie header fill Request.Cookies without SendDefaultPii. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

When
SendDefaultPiiis on, cookies are now parsed and filtered by name instead of being sent as the raw header. Sensitive values become[Filtered], and nameless or malformed cookies are replaced with[Filtered], as in sentry-java. This applies to the ASP.NET Core and ASP.NET scope/event paths and to the HTTP and GraphQL failed-request handlers.Cookie: user_session=abc; theme=dark-mode;opaque-tokenis now sent as:Where the array goes. The string array is the spec's span attribute, so it goes on server transactions through the existing
SetData. Error events have no attribute bag and Relay doesn't accept a plain string array inrequest.cookies, so events keep a filtered string, as sentry-java does. There is no public API or wire-format change, andSendDefaultPiistill gates collection. Client failed-request events only get the string.Worth a look in review:
[Filtered]segment. The other cookies are kept.FilterSetCookieHeaderhas no call site yet. Nothing collectsSet-Cookietoday, and starting to would be new data collection.SendDefaultPiirather thanDataCollection.Cookies, like every other cookie call site. It moves to the resolver with the rest of its integration, in DataCollection phase 2: migrate Sentry.AspNetCore #5430 for ASP.NET Core and DataCollection phase 2: migrate Sentry.AspNet (System.Web) #5431 for System.Web, so events and spans switch together.Cookievalue in the general header map is replaced with[Filtered]by fix: filter sensitive HTTP header values regardless of SendDefaultPii #5890 (DataCollection phase 0: always redact sensitive header values #5422), which is merged into this branch.SetDatawith astring[]needed an explicit branch in the JSON writer, because AOT builds have no serializer context for that type.Closes #5423
🤖 Generated with Claude Code