Skip to content

Commit e36c648

Browse files
committed
feat(data-collection): Request data collection
* Add `PII_HEADER_SNIPPETS` to backfill * Use `data_collection.http_headers.request` for ip address collection (both `env` and `request`) * Use `data_collection.cookies` for cookie collection (fix `Hash` signature since it always was one) * Sensitive headers like auth and cookies are not *ALWAYS* filtered unlike before
1 parent 4decfb9 commit e36c648

8 files changed

Lines changed: 114 additions & 68 deletions

File tree

sentry-ruby/lib/sentry/data_collection.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ class DataCollection
2828
# data_collection.stack_frame_variables = true
2929
# data_collection.frame_context_lines = 5
3030
# end
31+
3132
MODES = %i[off deny_list allow_list].freeze
33+
34+
PII_HEADER_SNIPPETS = %w[forwarded -ip _ip remote via _user -user].freeze
35+
3236
BODY_TYPES = %i[
3337
incoming_request
3438
outgoing_request
@@ -112,8 +116,10 @@ def self.backfill(configuration)
112116
# TODO-neel-data map to exact ruby behaviour for backwards compat behavior
113117
data_collection.user_info = false
114118
data_collection.cookies.mode = :off
115-
data_collection.http_headers.request.mode = :off
116-
data_collection.http_headers.response.mode = :off
119+
data_collection.http_headers.request.mode = :deny_list
120+
data_collection.http_headers.request.terms = PII_HEADER_SNIPPETS
121+
data_collection.http_headers.response.mode = :deny_list
122+
data_collection.http_headers.response.terms = PII_HEADER_SNIPPETS
117123
data_collection.http_bodies = []
118124
data_collection.url_query_params.mode = :off
119125
data_collection.graphql.document = false
@@ -132,7 +138,7 @@ def initialize
132138
request: KeyValueCollection.new(mode: :deny_list, terms: nil),
133139
response: KeyValueCollection.new(mode: :deny_list, terms: nil)
134140
)
135-
@http_bodies = nil
141+
@http_bodies = BODY_TYPES
136142
@url_query_params = KeyValueCollection.new(mode: :deny_list, terms: nil)
137143
@database_query_data = true
138144
@graphql = GraphQL.new(document: true, variables: true)

sentry-ruby/lib/sentry/data_collection/key_value_collection.rb

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ class KeyValueCollection
77
FILTERED_VALUE = "[Filtered]"
88

99
# Keys from this list are ALWAYS filtered, regardless of :mode
10-
# TODO-neel-data cookies have separate list, see JS
1110
SENSITIVE_DENY_LIST = %w[
1211
auth
1312
token
@@ -30,6 +29,30 @@ class KeyValueCollection
3029
set-cookie
3130
].freeze
3231

32+
# Additional terms applied to cookie names only. These cover common
33+
# opaque session, identity-provider, and load-balancer cookies without
34+
# making the general header denylist overly broad.
35+
SENSITIVE_COOKIE_NAME_DENY_LIST = %w[
36+
.sid
37+
sessid
38+
remember
39+
oidc
40+
pkce
41+
nonce
42+
__secure-
43+
__host-
44+
awsalb
45+
awselb
46+
akamai
47+
__stripe
48+
cognito
49+
firebase
50+
supabase
51+
sb-
52+
mfa
53+
2fa
54+
].freeze
55+
3356
# `mode` controls whether values are collected:
3457
# - `:off` disables collection.
3558
# - `:deny_list` collects values except those matching `terms`.
@@ -56,19 +79,19 @@ def terms=(terms)
5679
#
5780
# @param values [Hash] key-value data to filter
5881
# @return [Hash] a new filtered hash, or an empty hash when collection is off
59-
def filter(values)
82+
def filter(values, cookie: false)
6083
return {} if mode == :off
6184

6285
values.each_with_object({}) do |(key, value), filtered|
63-
filtered[key] = safe_value?(key) ? value : FILTERED_VALUE
86+
filtered[key] = safe_value?(key, cookie: cookie) ? value : FILTERED_VALUE
6487
end
6588
end
6689

6790
private
6891

69-
def safe_value?(key)
92+
def safe_value?(key, cookie: false)
7093
key_downcase = key.to_s.downcase
71-
return false if sensitive?(key_downcase)
94+
return false if sensitive?(key_downcase, cookie: cookie)
7295

7396
case mode
7497
when :deny_list
@@ -80,8 +103,9 @@ def safe_value?(key)
80103
end
81104
end
82105

83-
def sensitive?(key)
84-
SENSITIVE_DENY_LIST.any? { |term| key.include?(term) }
106+
def sensitive?(key, cookie: false)
107+
SENSITIVE_DENY_LIST.any? { |term| key.include?(term) } ||
108+
(cookie && SENSITIVE_COOKIE_NAME_DENY_LIST.any? { |term| key.include?(term) })
85109
end
86110

87111
def matches_any_term?(key)

sentry-ruby/lib/sentry/event.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,12 @@ def to_json_compatible
128128
private
129129

130130
def add_request_interface(env)
131-
@request = Sentry::RequestInterface.new(env: env, send_default_pii: @send_default_pii, rack_env_whitelist: @rack_env_whitelist)
131+
@request = Sentry::RequestInterface.new(
132+
env: env,
133+
data_collection: @data_collection,
134+
send_default_pii: @send_default_pii,
135+
rack_env_whitelist: @rack_env_whitelist
136+
)
132137
end
133138

134139
def serialize_attributes

sentry-ruby/lib/sentry/interfaces/request.rb

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,6 @@ module Sentry
44
class RequestInterface < Interface
55
REQUEST_ID_HEADERS = %w[action_dispatch.request_id HTTP_X_REQUEST_ID].freeze
66
CONTENT_HEADERS = %w[CONTENT_TYPE CONTENT_LENGTH].freeze
7-
IP_HEADERS = [
8-
"REMOTE_ADDR",
9-
"HTTP_CLIENT_IP",
10-
"HTTP_X_REAL_IP",
11-
"HTTP_X_FORWARDED_FOR"
12-
].freeze
137

148
# Regex to detect lowercase chars — match? is allocation-free (no MatchData/String)
159
LOWERCASE_PATTERN = /[a-z]/.freeze
@@ -30,7 +24,7 @@ class RequestInterface < Interface
3024
# @return [String]
3125
attr_accessor :query_string
3226

33-
# @return [String]
27+
# @return [Hash]
3428
attr_accessor :cookies
3529

3630
# @return [Hash]
@@ -40,33 +34,29 @@ class RequestInterface < Interface
4034
attr_accessor :env
4135

4236
# @param env [Hash]
43-
# @param send_default_pii [Boolean]
37+
# @param data_collection [DataCollection]
38+
# @param send_default_pii [Boolean] Deprecated compatibility input; still gates legacy cookie,
39+
# body, query-string, and Authorization header collection until those categories are migrated.
4440
# @param rack_env_whitelist [Array]
41+
# @see Configuration#data_collection
4542
# @see Configuration#send_default_pii
4643
# @see Configuration#rack_env_whitelist
47-
def initialize(env:, send_default_pii:, rack_env_whitelist:)
44+
def initialize(env:, data_collection:, send_default_pii:, rack_env_whitelist:)
4845
env = env.dup
49-
50-
unless send_default_pii
51-
# need to completely wipe out ip addresses
52-
RequestInterface::IP_HEADERS.each do |header|
53-
env.delete(header)
54-
end
55-
end
56-
5746
request = ::Rack::Request.new(env)
5847

5948
if send_default_pii
6049
self.data = read_data_from(request)
61-
self.cookies = request.cookies
6250
self.query_string = request.query_string
6351
end
6452

53+
self.cookies = data_collection.cookies.filter(request.cookies, cookie: true)
6554
self.url = request.scheme && request.url.split("?").first
6655
self.method = request.request_method
6756

68-
self.headers = filter_and_format_headers(env, send_default_pii)
69-
self.env = filter_and_format_env(env, rack_env_whitelist)
57+
collection = data_collection.http_headers.request
58+
self.headers = filter_and_format_headers(env, collection)
59+
self.env = filter_and_format_env(env, collection, rack_env_whitelist)
7060
end
7161

7262
private
@@ -86,14 +76,13 @@ def read_data_from(request)
8676
e.message
8777
end
8878

89-
def filter_and_format_headers(env, send_default_pii)
79+
def filter_and_format_headers(env, collection)
9080
env.each_with_object({}) do |(key, value), memo|
9181
begin
9282
key = key.to_s # rack env can contain symbols
9383
next memo["X-Request-Id"] ||= Utils::RequestId.read_from(env) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
9484
next if is_server_protocol?(key, value, env["SERVER_PROTOCOL"])
9585
next if is_skippable_header?(key)
96-
next if key == "HTTP_AUTHORIZATION" && !send_default_pii
9786

9887
# Rack stores headers as HTTP_WHAT_EVER, we need What-Ever
9988
key = key.delete_prefix("HTTP_")
@@ -107,12 +96,13 @@ def filter_and_format_headers(env, send_default_pii)
10796
Sentry.sdk_logger.warn(LOGGER_PROGNAME) { "Error raised while formatting headers: #{e.message}" }
10897
next
10998
end
99+
end.then do |e|
100+
collection.filter(e)
110101
end
111102
end
112103

113104
def is_skippable_header?(key)
114105
key.match?(LOWERCASE_PATTERN) || # lower-case envs aren't real http headers
115-
key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else
116106
!(key.start_with?("HTTP_") || CONTENT_HEADERS.include?(key))
117107
end
118108

@@ -134,11 +124,15 @@ def self.rack_3_or_above?
134124
Gem::Version.new(::Rack.release) >= Gem::Version.new("3.0")
135125
end
136126

137-
def filter_and_format_env(env, rack_env_whitelist)
138-
return env if rack_env_whitelist.empty?
139-
140-
env.select do |k, _v|
141-
rack_env_whitelist.include? k.to_s
127+
def filter_and_format_env(env, collection, rack_env_whitelist)
128+
if rack_env_whitelist.empty?
129+
env
130+
else
131+
env.select do |k, _v|
132+
rack_env_whitelist.include? k.to_s
133+
end
134+
end.then do |e|
135+
collection.filter(e)
142136
end
143137
end
144138
end

sentry-ruby/spec/sentry/data_collection/key_value_collection_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
it "normalizes terms assigned after initialization" do
5858
collection.terms = ["USER"]
5959

60-
expect(collection.filter("user_id" => "1")).to eq("user_id" => "[Filtered]")
60+
expect(collection.filter({ "user_id" => "1" })).to eq("user_id" => "[Filtered]")
6161
end
6262

6363
it "covers every built-in sensitive term" do

sentry-ruby/spec/sentry/data_collection_spec.rb

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010

1111
expect(data_collection.user_info).to eq(false)
1212
expect(data_collection.cookies.mode).to eq(:off)
13-
expect(data_collection.http_headers.request.mode).to eq(:off)
14-
expect(data_collection.http_headers.response.mode).to eq(:off)
13+
expect(data_collection.http_headers.request.mode).to eq(:deny_list)
14+
expect(data_collection.http_headers.request.terms).to eq(described_class::PII_HEADER_SNIPPETS)
15+
expect(data_collection.http_headers.response.mode).to eq(:deny_list)
16+
expect(data_collection.http_headers.request.terms).to eq(described_class::PII_HEADER_SNIPPETS)
1517
expect(data_collection.http_bodies).to eq([])
1618
expect(data_collection.url_query_params.mode).to eq(:off)
1719
expect(data_collection.database_query_data).to eq(false)
@@ -37,10 +39,10 @@
3739
expect(data_collection.cookies.mode).to eq(:deny_list)
3840
expect(data_collection.cookies.terms).to be_nil
3941
expect(data_collection.http_headers.request.mode).to eq(:deny_list)
40-
expect(data_collection.http_headers.request.terms).to be_nil
42+
expect(data_collection.http_headers.request.terms).to eq(nil)
4143
expect(data_collection.http_headers.response.mode).to eq(:deny_list)
42-
expect(data_collection.http_headers.response.terms).to be_nil
43-
expect(data_collection.http_bodies).to be_nil
44+
expect(data_collection.http_headers.request.terms).to eq(nil)
45+
expect(data_collection.http_bodies).to eq(described_class::BODY_TYPES)
4446
expect(data_collection.url_query_params.mode).to eq(:deny_list)
4547
expect(data_collection.url_query_params.terms).to be_nil
4648
expect(data_collection.database_query_data).to eq(true)

sentry-ruby/spec/sentry/event_spec.rb

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,24 +98,21 @@
9898
scope.apply_to_event(event)
9999

100100
expect(event.to_h[:request]).to eq(
101-
env: { 'SERVER_NAME' => 'localhost', 'SERVER_PORT' => '80' },
102-
headers: { 'Host' => 'localhost', 'X-Request-Id' => 'abcd-1234-abcd-1234' },
101+
env: { 'REMOTE_ADDR' => '[Filtered]', 'SERVER_NAME' => 'localhost', 'SERVER_PORT' => '80' },
102+
headers: { 'Host' => 'localhost', 'X-Forwarded-For' => '[Filtered]', 'X-Request-Id' => 'abcd-1234-abcd-1234' },
103103
method: 'POST',
104104
url: 'http://localhost/lol',
105+
cookies: {}
105106
)
106107
expect(event.to_h[:tags][:request_id]).to eq("abcd-1234-abcd-1234")
107108
expect(event.to_h[:user][:ip_address]).to eq(nil)
108109
end
109110

110-
it "removes ip address headers" do
111+
it "filters ip address headers from headers and env" do
111112
scope.apply_to_event(event)
112113

113-
# doesn't affect scope's rack_env
114-
expect(scope.rack_env).to include("REMOTE_ADDR")
115-
expect(event.request.headers.keys).not_to include("REMOTE_ADDR")
116-
expect(event.request.headers.keys).not_to include("Client-Ip")
117-
expect(event.request.headers.keys).not_to include("X-Real-Ip")
118-
expect(event.request.headers.keys).not_to include("X-Forwarded-For")
114+
expect(event.request.env).to include("REMOTE_ADDR" => "[Filtered]")
115+
expect(event.request.headers).to include("X-Forwarded-For" => "[Filtered]")
119116
end
120117
end
121118

0 commit comments

Comments
 (0)