Evolution of ActionController::Parameters from Rails 4 to 5
Upgrading a legacy Rails application often presents challenges, especially when migrating from Rails 4 to 5.
One significant evolution lies within the ActionController::Parameters
class, Rails 5 removes the Hash
inheritance which breaks application behavior. If you want to be prepared for that, keep reading this post.
Context
What if a user inspects the page’s HTML and adds a hidden field to increase the money they have in the bank? What if they add a field to make them an administrator?
Strong Parameters (ActionController::Parameters) are responsible for making parameters sent by the user permitted or not, according to our needs.
The Problem
At Rails 4, ActionController::Parameters
loses the permitted state when using the enumerable methods from the Hash
class, which is its parent class.
The Solution
Removing the inheritance was a hard decision to make as there were some applications
checking the type of the parameter using params.is_a?(Hash)
.
However, it was concluded that there wasn’t enough reason to not remove it.
Rails 4.2
The ActionController::Parameters
inherits from ActiveSupport::HashWithIndifferentAccess which inherits from Hash :
params = ActionController::Parameters.new(name: :foo)
params.class
# => ActionController::Parameters
params.class.superclass
# => ActiveSupport::HashWithIndifferentAccess
params.class.superclass.superclass
# => Hash
params.class.superclass.superclass.superclass
# => Object
Rails 5.0
The ActionController::Parameters
inherits from Object :
params = ActionController::Parameters.new(name: :foo)
params.class
# => ActionController::Parameters
params.class.superclass
# => Object
Side effects
As a result of the removal, there are some cases where the code might fail. Namely, if you are running into any of the following scenarios:
- Compare ActionController::Parameters with Hash
- Check Parameters’ Type with Hash
- Convert ActionController::Parameters to Hash
- Permitted ActionController::Parameters
- Hash methods Removal
- Workaround
Compare ActionController::Parameters
with Hash
Rails 4.2
When comparing the equality of Parameters
with a Hash
, it returns a boolean value.
params = ActionController::Parameters.new({foo: "foo", bar: "bar"})
# => {"foo"=>"foo", "bar"=>"bar"}
hash = {"foo"=>"foo", "bar"=>"bar"}
# => {"foo"=>"foo", "bar"=>"bar"}
params == hash
# => true
Rails 5.0
Overrides Hash#==
and a deprecation warning was introduced to alert users that the comparison with Hash
is not going to work on future versions.
params = ActionController::Parameters.new(foo: "foo", bar: "bar")
# => <ActionController::Parameters {"foo"=>"foo", "bar"=>"bar"} permitted: >
hash = {"foo"=>"foo", "bar"=>"bar"}
# => {"foo"=>"foo", "bar"=>"bar"}
params == hash
# DEPRECATION WARNING: Comparing equality between `ActionController::Parameters` and a `Hash` is deprecated and will be removed in Rails 5.1.
# Please only do comparisons between instances of `ActionController::Parameters`.
# If you need to compare to a hash, first convert it using `ActionController::Parameters#new`. (called from irb_binding at (irb):102)
# => true
The permitted
status is considered when comparing ActionController::Parameters
.
params = ActionController::Parameters.new(foo: :foo)
# => #<ActionController::Parameters {"foo"=>:foo} permitted: false>
permitted = ActionController::Parameters.new(foo: :foo).permit(:foo)
# => #<ActionController::Parameters {"foo"=>:foo} permitted: true>
params == permitted
# => false
params.permit(:foo) == permitted
# => true
Check Parameters'
Type with Hash
Rails 4.2
Checking Parameters
’ type was commonly used in this version.
params = ActionController::Parameters.new(foo: :foo)
# => {"foo" => :foo}
params.is_a?(Hash)
# => true
params.kind_of?(Hash)
# => true
params.is_a?(HashWithIndifferentAccess)
# => true
Rails 5
Since ActionController::Parameters
doesn’t inherit from Hash
anymore, the type check does not return true
anymore.
Interestingly, there was an intention to introduce a deprecation warning , but it wasn’t merged.
params = ActionController::Parameters.new(foo: :foo)
# => {"foo" => :foo}
params.is_a?(Hash)
# => false
params.kind_of?(Hash)
# => false
params.is_a?(HashWithIndifferentAccess)
# => false
Convert ActionController::Parameters
to Hash
If you need to convert your parameters to Hash
, there are some differences between versions that you should know about:
Rails 4.2
This version uses Hash#to_hash
and returns all the attributes no matter whether permitted
or not.
The to_h
method only returned the permitted
attributes.
params = ActionController::Parameters.new(foo: :foo, color: :red)
# => {"foo"=>:foo, "color"=>:red}
params.to_hash
# => {"foo"=>:foo, "color"=>:red}
params.to_h
# => {}
params.permit(:foo).to_h
# => {"foo"=>:foo}
params.to_hash.class
# => Hash
params.to_h.class
# => Hash
Rails 5.0
The to_hash
method was overridden and a deprecation warning was added .
In the next version it will check the permitted
status before returning a value.
The to_h
method returns an instance of HashWithIndifferentAccess
.
params = ActionController::Parameters.new(foo: :foo, color: :red)
# => <ActionController::Parameters {"foo"=>:foo, "color"=>:red} permitted: >
params.to_hash
# DEPRECATION WARNING: #to_hash unexpectedly ignores parameter filtering, and will change to enforce it in Rails 5.1. Enable `raise_on_unfiltered_parameters` to respect parameter filtering, which is the default in new applications. For the existing deprecated behaviour, call #to_unsafe_h instead. (called from irb_binding at (irb):49)
# => {"foo"=>:foo, "color"=>:red}
params.to_h
# => {}
params.permit(:foo).to_h
# => {"foo"=>:foo}
params.to_hash.class
# => Hash
params.to_h.class
# => ActiveSupport::HashWithIndifferentAccess
Rails 5.1
For both the to_hash
and to_h
methods, if non-permitted parameters are passed it raises ActionController::UnfilteredParameters
.
params = ActionController::Parameters.new(foo: :foo, color: :red)
# => <ActionController::Parameters {"foo"=>:foo, "color"=>:red} permitted: false>
params.to_hash
# ActionController::UnfilteredParameters (unable to convert unpermitted parameters to hash)
params.to_h
# ActionController::UnfilteredParameters (unable to convert unpermitted parameters to hash)
params.permit(:foo).to_hash
# => {"foo"=>:foo}
params.permit(:foo).to_h
# => {"foo"=>:foo}
params.permit(:foo).to_hash.class
# => Hash
params.permit(:foo).to_h.class
# => ActiveSupport::HashWithIndifferentAccess
Permitted ActionController::Parameters
To stay safe the permitted
status is initialized as false
, that way you have the ability to use the permit
method to permit parameters as you prefer. Ex. params.permit(:foo)
.
Rails 4.2
It is initialized as false
.
params = ActionController::Parameters.new(foo: :bar)
# => {"foo" => :bar}
params.permitted?
# => false
Rails 5.0
It is initialized as nil
.
params = ActionController::Parameters.new(foo: :bar)
# => <ActionController::Parameters {"foo"=>:bar} permitted: >
params.permitted?
# => nil
Rails 5.1
It is initialized as false
as it used to be in Rails 4.
params = ActionController::Parameters.new(foo: :bar)
# => <ActionController::Parameters {"foo"=>:bar} permitted: false>
params.permitted?
# => false
There is the permit_all_parameters
configuration key that allows all parameters be permitted (which is not recommended).
Below is an example of how it works. You can also find more information here.
params = ActionController::Parameters.new(name: "Juan")
params.permitted? # => false
Person.new(params) # => ActiveModel::ForbiddenAttributesError
ActionController::Parameters.permit_all_parameters = true
params = ActionController::Parameters.new(name: "Juan")
params.permitted? # => true
Person.new(params) # => #<Person id: nil, name: "Juan">
Hash Methods Removal
Rails 4.0
ActionController::Parameters
used to respond to Hash
methods because it inherited from the Hash
class.
params = ActionController::Parameters.new(foo: :foo, color: :red)
# => {"foo"=> :foo, "color" => :red}
params.except!(:foo)
# => {"color" => :red}
Rails 5.0
A deprecation warning was added because the ActionController::Parameters
objects are not going to respond to Hash
methods in Rails 5.1.
params = ActionController::Parameters.new(foo: :foo, color: :red)
# => <ActionController::Parameters {"foo"=>:foo, "color" => :red} permitted: >
params.except!(:foo)
# DEPRECATION WARNING: Method except! is deprecated and will be removed in Rails 5.1, as `ActionController::Parameters` no longer inherits from hash.
# Using this deprecated behavior exposes potential security problems.
# If you continue to use this method you may be creating a security vulnerability in your app that can be exploited.
# Instead, consider using one of these documented methods which are not deprecated:
# http://api.rubyonrails.org/v5.0.7.2/classes/ActionController/Parameters.html (called from irb_binding at (irb):50)
# => {"color" => :red}
Rails 5.1
ActionController::Parameters
objects no longer respond to Hash
methods.
params = ActionController::Parameters.new(foo: :foo, color: :red)
# => <ActionController::Parameters {"foo"=>:foo, "color"=>:red} permitted: false>
params.except!(:color)
# NoMethodError (undefined method `except!' for #<ActionController::Parameters:0x000055c04bf7f6a0>)
# Did you mean? except
Workaround
Use to_unsafe_h or its alias, to_unsafe_hash
,
to return an unfiltered Hash
representation of a Parameters
object.
It is not recommended to keep this change as it would remove the protection for strong parameters that Rails 5 is introducing. It’s just a workaround to allow the upgrade to Rails 5, then parameters can be updated progressively.
Rails 4.2
params = ActionController::Parameters.new("foo" => "foo", "color" => "red")
# => {"foo"=>"foo", "color"=>"red"}
params.to_unsafe_h
# => {"foo"=>"foo", "color"=>"red"}
params.to_unsafe_h.to_hash
# => {"foo"=>"foo", "color"=>"red"}
params.to_unsafe_h.except!("color")
# => {"foo"=>"foo"}
params.to_unsafe_h.class
# => Hash
Rails 5
params = ActionController::Parameters.new("foo" => "foo", "color" => "red")
# => <ActionController::Parameters {"foo"=>"foo", "color"=>"red"} permitted: >
params.to_unsafe_h
# => {"foo"=>"foo", "color"=>"red"}
params.to_unsafe_h.to_hash
# => {"foo"=>"foo", "color"=>"red"}
params.to_unsafe_h.except!("color")
# => {"foo"=>"foo"}
params.to_unsafe_h.class
# => ActiveSupport::HashWithIndifferentAccess
Conclusion
The modifications made to ActionController::Parameters
from Rails 4 to 5 have the potential to cause application failures if not handled with caution.
However, these changes play a crucial role in resolving security issues that exist in Rails 4.
Still running Rails 4 and wanting to upgrade to the latest Rails? We can help!