/
cleaner.rb
192 lines (168 loc) · 5.19 KB
/
cleaner.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
require 'uri'
module Bugsnag
# @api private
class Cleaner
FILTERED = '[FILTERED]'.freeze
RECURSION = '[RECURSION]'.freeze
OBJECT = '[OBJECT]'.freeze
RAISED = '[RAISED]'.freeze
OBJECT_WITH_ID_AND_CLASS = '[OBJECT]: [Class]: %<class_name>s [ID]: %<id>d'.freeze
##
# @param configuration [Configuration]
def initialize(configuration)
@configuration = configuration
end
def clean_object(object)
@deep_filters = deep_filters?
traverse_object(object, {}, nil)
end
##
# @param url [String]
# @return [String]
def clean_url(url)
return url if @configuration.meta_data_filters.empty?
uri = URI(url)
return url unless uri.query
query_params = uri.query.split('&').map { |pair| pair.split('=') }
query_params.map! do |key, val|
if filters_match?(key)
"#{key}=#{FILTERED}"
else
"#{key}=#{val}"
end
end
uri.query = query_params.join('&')
uri.to_s
end
private
##
# This method calculates whether we need to filter deeply or not; i.e. whether
# we should match both with and without 'request.params'
#
# This is cached on the instance variable '@deep_filters' for performance
# reasons
#
# @return [Boolean]
def deep_filters?
@configuration.meta_data_filters.any? do |filter|
filter.is_a?(Regexp) && filter.to_s.include?("\\.".freeze)
end
end
def clean_string(str)
if defined?(str.encoding) && defined?(Encoding::UTF_8)
if str.encoding == Encoding::UTF_8
str.valid_encoding? ? str : str.encode('utf-16', invalid: :replace, undef: :replace).encode('utf-8')
else
str.encode('utf-8', invalid: :replace, undef: :replace)
end
elsif defined?(Iconv)
Iconv.conv('UTF-8//IGNORE', 'UTF-8', str) || str
else
str
end
end
def traverse_object(obj, seen, scope)
return nil if obj.nil?
# Protect against recursion of recursable items
protection = if obj.is_a?(Hash) || obj.is_a?(Array) || obj.is_a?(Set)
return seen[obj] if seen[obj]
seen[obj] = RECURSION
end
value = case obj
when Hash
clean_hash = {}
obj.each do |k, v|
begin
current_scope = [scope, k].compact.join('.')
if filters_match_deeply?(k, current_scope)
clean_hash[k] = FILTERED
else
clean_hash[k] = traverse_object(v, seen, current_scope)
end
# If we get an error here, we assume the key needs to be filtered
# to avoid leaking things we shouldn't. We also remove the key itself
# because it may cause issues later e.g. when being converted to JSON
rescue StandardError
clean_hash[RAISED] = FILTERED
rescue SystemStackError
clean_hash[RECURSION] = FILTERED
end
end
clean_hash
when Array, Set
obj.map { |el| traverse_object(el, seen, scope) }
when Numeric, TrueClass, FalseClass
obj
when String
clean_string(obj)
else
# guard against objects that raise or blow the stack when stringified
begin
str = obj.to_s
rescue StandardError
str = RAISED
rescue SystemStackError
str = RECURSION
end
# avoid leaking potentially sensitive data from objects' #inspect output
if str =~ /#<.*>/
# Use id of the object if available
if obj.respond_to?(:id)
format(OBJECT_WITH_ID_AND_CLASS, class_name: obj.class, id: obj.id)
else
OBJECT
end
else
clean_string(str)
end
end
seen[obj] = value if protection
value
end
##
# @param key [String, #to_s]
# @return [Boolean]
def filters_match?(key)
str = key.to_s
@configuration.meta_data_filters.any? do |filter|
case filter
when Regexp
str.match(filter)
else
str.include?(filter.to_s)
end
end
end
##
# If someone has a Rails filter like /^stuff\.secret/, it won't match
# "request.params.stuff.secret", so we try it both with and without the
# "request.params." bit.
#
# @param key [String, #to_s]
# @param scope [String]
# @return [Boolean]
def filters_match_deeply?(key, scope)
return false unless scope_should_be_filtered?(scope)
return true if filters_match?(key)
return false unless @deep_filters
return true if filters_match?(scope)
@configuration.scopes_to_filter.any? do |scope_to_filter|
if scope.start_with?("#{scope_to_filter}.request.params.")
filters_match?(scope.sub("#{scope_to_filter}.request.params.", ''))
else
filters_match?(scope.sub("#{scope_to_filter}.", ''))
end
end
end
##
# Should the given scope be filtered?
#
# @param scope [String]
# @return [Boolean]
def scope_should_be_filtered?(scope)
@configuration.scopes_to_filter.any? do |scope_to_filter|
scope.start_with?("#{scope_to_filter}.")
end
end
end
end