Search code examples
ruby-on-railsrubyairbrake

Airbrake throws error one of the patterns in Airbrake::Filters::KeysBlacklist is invalid when using custom proc


In our application we want to filter certain parameters differently depending on their usage... for example passwords should always be filtered entirely, but for email and name we want to retain parts of the value for debugging and support.

To do this, we've used a custom proc to format them:

1_filter_parameter_logging.rb:

Rails.application.config.filter_parameters += [
  :password
]

Rails.application.config.filter_parameters << proc do |param_name, value|
  if %w[email name].include?(param_name)
    if param_name.to_s == 'email'
      value.gsub!(/(?<=^.{2}).*?(?=@)/, '*') # test partial filter
    else
      value.replace('[FILTERED_CUSTOM]') # test custom filter
    end
  end
end

Then in 2_airbrake.rb we use these settings as so:

c.blacklist_keys = Rails.application.config.filter_parameters

To test this theory, we deliberately throw an exception in our app with this example controller action:

class ExampleController
  def show
    raise StandardError, 'Testing Filter Parameter Logging'
  end
end

And then make a simple request to that endpoint:

http://localhost:3000/example?name=cameron&[email protected]&password=test

In our logs we see the following:

Processing by ExampleController#show as HTML
  Parameters: {"name"=>"[FILTERED_CUSTOM]", "email"=>"ca*@test.com", "password"=>"[FILTERED]"}

And then an error from Airbake:

**Airbrake: one of the patterns in Airbrake::Filters::KeysBlacklist is invalid. Known patterns: [:password, nil]

And if we look at the error in Airbrake we see that it hasn't filtered them:

{
  "action" => "show",
  "controller" => "example",
  "email" => "[email protected]",
  "name" => "cameron",
  "password" => "[Filtered]"
}

Why is Airbrake throwing that error, seems Rails is able to filter those parameters using that proc based on those logs, but then Airbrake fails to filter them and allows them through.

Airbrake doesn't explain exactly what's invalid with the pattern... just that it doesn't see it as valid, which isn't very helpful.


Solution

  • The param filtering offered by Rails is different than Airbrake.

    Rails allows for complex param filtering. You can define a proc that takes in the parameter key and value as arguments and returns the filtered value. The execution of the proc happens here in ActiveSupport::ParameterFilter#value_for_key.

    Airbrake is more limited. It can only filter parameter names by comparing them against a String, Symbol, or Regexp. The documentation indicates that you can define a proc but it only gets executed once on the first notification and:

    The Proc must return an Array consisting only of the elements, which are considered to be valid for this option.

    The proc is executed here. It is called without any arguments. If your example proc is called without arguments it returns nil. Since this doesn't match a String, Symbol, or Regexp an exception is raised here.

    Airbrake uses a should_filter? method that takes just the param key. If that method returns true the "[Filtered]" string replaces the value. This existing structure isn't easy to quickly override to get the behavior you want. But you could try to completely override the Airbrake::Filters::KeysBlocklist class and Airbrake::Filters::KeysFilter module.

    Note that the info here and all the links are for the current version of airbrake-ruby. blacklist_keys was replaced by blocklist_keys several years ago. And classes like KeysBlacklist were replaced by KeysBlocklist. But these were just naming changes and functionality has remained relatively the same over time.


    Rather than override the standard Airbrake param filtering, you're probably better off trying to add a custom filter. Here's an example of what you could try to put into an initializer in your app:

    Airbrake.add_filter do |notice|
      notice[:params].each do |key, value|
        if key == :email
          value.gsub!(/(?<=^.{2}).*?(?=@)/, '*')  # test partial filter
        elsif key == :name
          value.replace('[FILTERED_CUSTOM]')  # test custom filter
        end
      end
    end