Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src/Sentry/Internal/DataCollection/KeyValueFilterBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace Sentry.Internal.DataCollection;

/// <summary>
/// How a key-value collection (HTTP headers, cookies, URL query parameters) should be collected, per the
/// DataCollection spec: https://develop.sentry.dev/sdk/foundations/client/data-collection/
/// </summary>
internal enum KeyValueFilterMode
{
/// <summary>Neither keys nor values are collected.</summary>
Off,

/// <summary>All keys are collected; values of sensitive keys are replaced with "[Filtered]".</summary>
DenyList,

/// <summary>All keys are collected; only values of allow-listed, non-sensitive keys are kept.</summary>
AllowList
}

/// <summary>
/// A <see cref="KeyValueFilterMode"/> plus the terms that parameterize it: extra deny terms in
/// <see cref="KeyValueFilterMode.DenyList"/> mode, allowed key terms in <see cref="KeyValueFilterMode.AllowList"/>
/// mode. Terms match key names as case-insensitive substrings. Internal precursor to the public behavior type that
/// ships with the DataCollection option.
/// </summary>
internal readonly struct KeyValueFilterBehavior
{
private readonly string[]? _terms;

private KeyValueFilterBehavior(KeyValueFilterMode mode, string[]? terms)
{
Mode = mode;
_terms = terms;
}

public KeyValueFilterMode Mode { get; }

public string[] Terms => _terms ?? [];

public static KeyValueFilterBehavior Off => new(KeyValueFilterMode.Off, null);

/// <summary>The default behavior: collect everything, replacing values of sensitive keys.</summary>
public static KeyValueFilterBehavior DenyList => new(KeyValueFilterMode.DenyList, null);

public static KeyValueFilterBehavior DenyListWith(params string[] extraDenyTerms) =>
new(KeyValueFilterMode.DenyList, extraDenyTerms);

public static KeyValueFilterBehavior AllowList(params string[] allowedTerms) =>
new(KeyValueFilterMode.AllowList, allowedTerms);
}
148 changes: 148 additions & 0 deletions src/Sentry/Internal/DataCollection/SensitiveDataFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
namespace Sentry.Internal.DataCollection;

/// <summary>
/// Filters automatically collected key-value data (HTTP headers, cookies, URL query parameters) so that values of
/// sensitive keys are replaced with "[Filtered]" before being sent to Sentry, per the DataCollection spec:
/// https://develop.sentry.dev/sdk/foundations/client/data-collection/
/// Key names are always preserved; only values are replaced. Only applies to automatically collected data —
/// data explicitly set by the user is never filtered.
/// </summary>
internal static class SensitiveDataFilter
{
internal const string FilteredValue = PiiExtensions.RedactedText;

/// <summary>
/// Case-insensitive substrings identifying sensitive key names, from the canonical denylist in the spec,
/// plus "set-cookie"/"cookie" so cookie headers are treated as sensitive wherever individual cookie
/// pairs cannot be extracted.
/// </summary>
internal static readonly string[] SensitiveKeyTerms =
[
"auth",
"token",
"secret",
"session",
"password",
"passwd",
"pwd",
"key",
"jwt",
"bearer",
"sso",
"saml",
"csrf",
"xsrf",
"credentials",
"sid",
"identity",
"set-cookie",
"cookie",
];

/// <summary>
/// Extra substrings matched only against individual Cookie / Set-Cookie names (not header names), covering
/// common session secrets that do not match <see cref="SensitiveKeyTerms"/> (e.g. "connect.sid") without
/// false positives on arbitrary HTTP headers. Cookie-only terms already implied by a
/// <see cref="SensitiveKeyTerms"/> match (e.g. "oauth", "id_token") are omitted.
/// </summary>
internal static readonly string[] SensitiveCookieNameTerms =
[
// Express / Connect default session cookie
".sid",
// Opaque session ids (PHPSESSID, ASPSESSIONID*, *sessid*, ...)
"sessid",
// "Remember me" tokens
"remember",
// OIDC / OAuth auxiliary ("oauth*" is covered by "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",
];

/// <summary>
/// Key-name substrings that identify end users (IP addresses, forwarding chains). Not filtered by default;
/// used to extend deny lists when replicating <c>SendDefaultPii = false</c> behavior (GDPR terms per the spec).
/// </summary>
internal static readonly string[] PiiKeyTerms = ["forwarded", "-ip", "remote-", "via", "-user"];

internal static bool IsSensitiveKey(string key) => MatchesAny(key, SensitiveKeyTerms);

/// <summary>
/// Filters a single value according to <paramref name="behavior"/>. Returns the value unchanged, or
/// <see cref="FilteredValue"/> in its place. Must not be called in <see cref="KeyValueFilterMode.Off"/> mode
/// (in off mode nothing should be collected at all).
/// </summary>
internal static string FilterValue(string key, string value, KeyValueFilterBehavior behavior,
string[]? additionalDenyTerms = null)
{
Debug.Assert(behavior.Mode != KeyValueFilterMode.Off,
"FilterValue must not be called in Off mode - collect nothing instead.");

if (MatchesAny(key, SensitiveKeyTerms) ||
(additionalDenyTerms is not null && MatchesAny(key, additionalDenyTerms)))
{
return FilteredValue;
}

return behavior.Mode switch
{
KeyValueFilterMode.DenyList => MatchesAny(key, behavior.Terms) ? FilteredValue : value,
KeyValueFilterMode.AllowList => MatchesAny(key, behavior.Terms) ? value : FilteredValue,
_ => value
};
}

/// <summary>
/// Filters key-value data according to <paramref name="behavior"/>. Key names are always preserved; values are
/// either kept or replaced with <see cref="FilteredValue"/>. In <see cref="KeyValueFilterMode.Off"/> mode an
/// empty dictionary is returned. <paramref name="additionalDenyTerms"/> are extra sensitive terms applied in
/// every mode, beyond the built-in denylist (e.g. <see cref="SensitiveCookieNameTerms"/> when filtering cookies).
/// </summary>
internal static Dictionary<string, string> FilterKeyValueData(
IEnumerable<KeyValuePair<string, string>> data,
KeyValueFilterBehavior behavior,
string[]? additionalDenyTerms = null)
{
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (behavior.Mode == KeyValueFilterMode.Off)
{
return result;
}

foreach (var (key, value) in data)
{
result[key] = FilterValue(key, value, behavior, additionalDenyTerms);
}

return result;
}

private static bool MatchesAny(string key, string[] terms)
{
foreach (var term in terms)
{
if (key.Contains(term, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

return false;
}
}
Loading
Loading