Skip to content
Merged
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,14 @@ Sentry.init do |config|
filter = Blouson::SentryParameterFilter.new(filter_pattern, secure_headers)

config.before_send = lambda do |event, _hint|
filter.process(event.to_hash)
filter.process(event)
end
end

```

**Note:** Since sentry-ruby v6, `event.to_hash` is no longer available. Pass `event` directly to `filter.process` instead of `filter.process(event.to_hash)`.

### SensitiveMailLogFilter
ActionMailer outputs email address, all headers, and body text to the log when sending email.

Expand Down
2 changes: 1 addition & 1 deletion blouson.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'arproxy', '>= 1.0.0'
spec.add_development_dependency 'mysql2'
spec.add_development_dependency 'pry'
spec.add_development_dependency 'sentry-ruby'
spec.add_development_dependency 'sentry-ruby', '>= 6.0'

spec.add_development_dependency 'appraisal'
spec.add_development_dependency "bundler", ">= 1.14"
Expand Down
63 changes: 34 additions & 29 deletions lib/blouson/sentry_parameter_filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,53 +17,58 @@ def process(event)
private

def process_request_body(event)
if event[:request] && event[:request][:data].present?
data = event[:request][:data]
if data.is_a?(String)
# Maybe JSON request
begin
data = JSON.parse(data)
event[:request][:data] = JSON.dump(@parameter_filter.filter(data))
rescue JSON::ParserError => e
# Record parser error to extra field
event[:extra]['BlousonError'] = e.message
end
else
event[:request][:data] = @parameter_filter.filter(data)
req = event.request
return unless req && req.data.present?

Comment thread
os0x marked this conversation as resolved.
data = req.data
if data.is_a?(String)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Memo: I think it's okay to leave this condition as is, but it seems that Sentry::RequestInterface expects data is a Hash: https://github.com/getsentry/sentry-ruby/blob/a1c52820777d5ddb151ba66fd55cced0732730fd/sentry-ruby/lib/sentry/interfaces/request.rb#L24-L26

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Although it's annotated as @return [Hash] , looking at the read_data_from implementation, there are several cases where it returns a String:

  • Non-form-data requests (e.g. JSON): request.body.read → String
  • Non-rewindable body: "Skipped non-rewindable request body" → String
  • IOError: e.message → String

So the data.is_a?(String) check is still necessary.

# Maybe JSON request
begin
data = JSON.parse(data)
req.data = JSON.dump(@parameter_filter.filter(data))
rescue JSON::ParserError => e
# Record parser error to extra field
event.extra['BlousonError'] = e.message
end
else
req.data = @parameter_filter.filter(data)
end
end

def process_query_string(event)
if event[:request] && event[:request][:query_string].present?
query = Rack::Utils.parse_query(event[:request][:query_string])
filtered = @parameter_filter.filter(query)
req = event.request
return unless req && req.query_string.present?

event[:request][:query_string] = Rack::Utils.build_query(filtered)
end
query = Rack::Utils.parse_query(req.query_string)
filtered = @parameter_filter.filter(query)

req.query_string = Rack::Utils.build_query(filtered)
end

def process_request_header(event)
if event[:request] && event[:request][:headers]
headers = event[:request][:headers]
headers.each_key do |k|
if @header_filters.include?(k.downcase)
headers[k] = 'FILTERED'
end
req = event.request
return unless req && req.headers

req.headers.each_key do |k|
if @header_filters.include?(k.downcase)
req.headers[k] = 'FILTERED'
end
end
end

def process_cookie(event)
if (cookies = event.dig(:request, :cookies))
event[:request][:cookies] = @parameter_filter.filter(cookies)
req = event.request
return unless req

if req.cookies
req.cookies = @parameter_filter.filter(req.cookies)
end

if event[:request] && event[:request][:headers] && event[:request][:headers]['Cookie']
cookies = Hash[event[:request][:headers]['Cookie'].split('; ').map { |pair| pair.split('=', 2) }]
if req.headers && req.headers['Cookie']
cookies = Hash[req.headers['Cookie'].split('; ').map { |pair| pair.split('=', 2) }]
filtered = @parameter_filter.filter(cookies)

event[:request][:headers]['Cookie'] = filtered.map { |pair| pair.join('=') }.join('; ')
req.headers['Cookie'] = filtered.map { |pair| pair.join('=') }.join('; ')
end
end
end
Expand Down
51 changes: 28 additions & 23 deletions spec/blouson/sentry_parameter_filter_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'spec_helper'
require 'sentry-ruby'
require 'rack/utils'
require 'blouson/sentry_parameter_filter'

RSpec.describe Blouson::SentryParameterFilter do
Expand All @@ -8,69 +9,73 @@
let(:filter_class) { described_class.new(filters, header_filters) }

let(:event) do
{
request: {
headers: {
'Really-Sensitive-Header-That-Needs-To-Be-Filtered' => 'important_token',
'Insensitive-Header' => 'foo',
'Cookie' => 'sensitive-data=secret-value; foo=non-secret-value'
},
cookies: {
'sensitive-data' => 'secret-value',
'foo' => 'non-secret-value'
},
data: "{\"sensitive-data\":\"secret-value\",\"normal-data\": {\"some-sensitive-data\":\"secret-value\"}}",
query_string: 'sensitive-data=secret-value&normal-data=non-secret-value',
}
config = Sentry::Configuration.new
config.send_default_pii = true
ev = Sentry::ErrorEvent.new(configuration: config)

request = Sentry::RequestInterface.allocate
request.headers = {
'Really-Sensitive-Header-That-Needs-To-Be-Filtered' => 'important_token',
'Insensitive-Header' => 'foo',
'Cookie' => 'sensitive-data=secret-value; foo=non-secret-value'
}
request.cookies = {
'sensitive-data' => 'secret-value',
'foo' => 'non-secret-value'
}
request.data = "{\"sensitive-data\":\"secret-value\",\"normal-data\": {\"some-sensitive-data\":\"secret-value\"}}"
request.query_string = 'sensitive-data=secret-value&normal-data=non-secret-value'
ev.instance_variable_set(:@request, request)

ev
end

describe 'process_request_body' do
it 'filters request body in the filters' do
processed_event = filter_class.process(event)
data = JSON.parse(processed_event[:request][:data])
data = JSON.parse(processed_event.request.data)
expect(data['sensitive-data']).to eq('[FILTERED]')
end

it 'do not filters request body not in the filters but nested value is filtered' do
processed_event = filter_class.process(event)
data = JSON.parse(processed_event[:request][:data])
data = JSON.parse(processed_event.request.data)
expect(data['normal-data']).to eq({ 'some-sensitive-data' => '[FILTERED]' })
end
end

describe 'process_query_string' do
it 'filters query string in the filters' do
processed_event = filter_class.process(event)
query = Rack::Utils.parse_query(processed_event[:request][:query_string])
query = Rack::Utils.parse_query(processed_event.request.query_string)
expect(query['sensitive-data']).to eq('[FILTERED]')
end

it 'do not filters query string not in the filters' do
processed_event = filter_class.process(event)
query = Rack::Utils.parse_query(processed_event[:request][:query_string])
query = Rack::Utils.parse_query(processed_event.request.query_string)
expect(query['normal-data']).to eq('non-secret-value')
end
end

describe 'process_request_header' do
it 'filters headers in the header_filters' do
processed_event = filter_class.process(event)
expect(processed_event[:request][:headers]['Really-Sensitive-Header-That-Needs-To-Be-Filtered']).to eq('FILTERED')
expect(processed_event.request.headers['Really-Sensitive-Header-That-Needs-To-Be-Filtered']).to eq('FILTERED')
end

it 'do not filter headers not in header_filters' do
processed_event = filter_class.process(event)
expect(processed_event[:request][:headers]['Insensitive-Header']).to eq('foo')
expect(processed_event.request.headers['Insensitive-Header']).to eq('foo')
end
end

describe 'process_cookie' do
it 'filters values in cookie in filters' do
processed_event = filter_class.process(event)
expect(processed_event[:request][:cookies]['sensitive-data']).to eq('[FILTERED]')
expect(processed_event[:request][:cookies]['foo']).to eq('non-secret-value')
expect(processed_event[:request][:headers]['Cookie']).to eq('sensitive-data=[FILTERED]; foo=non-secret-value')
expect(processed_event.request.cookies['sensitive-data']).to eq('[FILTERED]')
expect(processed_event.request.cookies['foo']).to eq('non-secret-value')
expect(processed_event.request.headers['Cookie']).to eq('sensitive-data=[FILTERED]; foo=non-secret-value')
end
end
end