rails / rails

Ruby on Rails
https://rubyonrails.org
MIT License
56k stars 21.66k forks source link

ActiveRecord::Encryption is encrypting/filtering other similar named attributes #51254

Open keshavbiswa opened 8 months ago

keshavbiswa commented 8 months ago

Steps to reproduce

This outputs

=> #<User:0x000000010c5ea648 id: nil, first_name: "[FILTERED]", last_name: "World", email: nil, other_first_name: "[FILTERED]", created_at: nil, updated_at: nil>

Encrypted attributes only has :first_name in the set

User.encrypted_attributes => <Set: {:first_name}>


<img width="654" alt="Screenshot 2024-03-05 at 7 21 18 PM" src="https://github.com/rails/rails/assets/27268721/bbb18517-a068-4982-97ea-39cbd5f492a5">

### Expected behavior
- `other_first_name` should not be [FILTERED]

### Actual behavior
- `other_first_name` is `[FILTERED]`

### System configuration
**Rails version**: `Edge`

**Ruby version**: 3.2.2
hahmed commented 8 months ago

This is not related to encryption, but rather parameter filtering, for example a new rails app ssn is part of the param filtering by default:

> Rails.application.config.filter_parameters
=> [:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn]

Create a model with ssn prefix/suffix, the output is:

#<Staff:0x0000000109c79f48
 id: nil,
 first_name: "tim",
 ssn_name: "[FILTERED]",
 ssn: "[FILTERED]",
 other_ssn_name: "[FILTERED]",
 name_ssn: "[FILTERED]",
 created_at: nil,
 updated_at: nil>
keshavbiswa commented 8 months ago

Did a little bit of debugging and it turns out something expected but really shouldn't be:

params = ActionController::Parameters.new(first_name: "Hello", other_first_name: "Hello", last_name: "World")
param_filter = ActiveSupport::ParameterFilter.new([:first_name])
=> #<ActiveSupport::ParameterFilter:0x0000000107ae67a0 @blocks=nil, @deep_regexps=nil, @mask="[FILTERED]", @no_filters=false, @regexps=[/first_name/i]>

# #filter_param is filtering both first_name and other_first_name
param_filter.filter_param("first_name", "Value")
=> "[FILTERED]"
param_filter.filter_param("other_first_name", "Value")
=> "[FILTERED]"
param_filter.filter_param("last_name", "Value")
=> "Value"

# #filter is filtering both first_name and other_first_name
param_filter.filter(params)
=> #<ActionController::Parameters {"first_name"=>"[FILTERED]", "other_first_name"=>"[FILTERED]", "last_name"=>"World"} permitted: false>

The reason why is simple:

/first_name/i.match?("first_name")
=> true
/first_name/i.match?("other_first_name")
=> true
keshavbiswa commented 8 months ago
=> [:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn]

I mean that's fine from ParamsFiltering concept and for explicitly provided filter_parameters values. But if we're encrypting attributes, we always would want exact text matches.

I'd suggest we create filter_exact_param method which can be used instead of filter_param that does exact text matching for encrypted attributes.

jaspermayone commented 7 months ago

I am also experiencing this with :email and :email_is_confirmed which is not in the ParamsFiltering

alexplatteeuw commented 7 months ago

I mean that's fine from ParamsFiltering concept and for explicitly provided filter_parameters values. But if we're encrypting attributes, we always would want exact text matches.

I just got bitten by the same problem. I agree that it's surprising regarding encryption. Especially since there's an option called excluded_from_filter_parameters that allows excluding the encrypted attribute (which also excludes the partial matching attribute), but it doesn't work for excluding the partial matching attribute on its own.

jaspermayone commented 7 months ago

Can anyone on the rails team provide any insights here?

DickyMacias commented 5 months ago

@keshavbiswa I think this is due ignore_case: true option. This option allow you to have a non searchable column where you can save the value for the main column in case sensitive, and the you can read this new one when you search in the main column that is downcased.

A little bit explain is in this blog in the Case Sensitive Search part: https://hint.io/blog/Active-Record-Encryption

They use this option to see what is the backup column.