Skip to content

fix: Filter sensitive cookie values instead of sending raw cookies - #5894

Open
jamescrosswell wants to merge 8 commits into
mainfrom
ai/data-collection-parse-cookies-196ba8
Open

jamescrosswell wants to merge 8 commits into
mainfrom
ai/data-collection-parse-cookies-196ba8

Conversation

@jamescrosswell

@jamescrosswell jamescrosswell commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

When SendDefaultPii is 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-token is now sent as:

request.cookies:              "user_session=[Filtered]; theme=dark-mode; [Filtered]"
http.request.header.cookie:   ["user_session=[Filtered]", "theme=dark-mode", "[Filtered]"]

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 in request.cookies, so events keep a filtered string, as sentry-java does. There is no public API or wire-format change, and SendDefaultPii still gates collection. Client failed-request events only get the string.

Worth a look in review:

Closes #5423

🤖 Generated with Claude Code

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

codecov Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.70130% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 76.95%. Comparing base (837511d) to head (146c1bc).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/Sentry/Internal/DataCollection/CookieFilter.cs 98.03% 0 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

jamescrosswell and others added 3 commits November 9, 2026 16:37
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>
@jamescrosswell
jamescrosswell marked this pull request as ready for review November 10, 2026 07:57
@github-actions github-actions Bot added the risk: medium PR risk score: medium label Nov 10, 2026
return SensitiveDataFilter.FilteredValue;
}

return $"{name}={SensitiveDataFilter.FilterValue(name, value, behavior, SensitiveDataFilter.SensitiveCookieNameTerms)}";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Suggested change
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) =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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")]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
[InlineData("theme=dark, user_session=abc")]
[InlineData("theme=dark, user_session=abc")]
[InlineData("theme=dark,user_session=abc")]

}

[Fact]
public void FilterCookieHeader_MultiplePairs_NeverContainsSensitiveValue()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DataCollection phase 0: parse cookies individually and filter sensitive cookie names

2 participants