Evolution of ActionController::Parameters from Rails 4 to 5

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 opens a new window 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 opens a new window checking the type of the parameter using params.is_a?(Hash). However, it was concluded opens a new window that there wasn’t enough reason to not remove it.

Rails 4.2

The ActionController::Parameters inherits from ActiveSupport::HashWithIndifferentAccess opens a new window which inherits from Hash opens a new window :

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 opens a new window :

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

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#== opens a new window and a deprecation warning opens a new window 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 opens a new window 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 opens a new window , 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 opens a new window 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 opens a new window . 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 opens a new window 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 opens a new window as false.

params = ActionController::Parameters.new(foo: :bar)
# => {"foo" => :bar}
params.permitted?
# => false

Rails 5.0

It is initialized opens a new window as nil.

params = ActionController::Parameters.new(foo: :bar)
# => <ActionController::Parameters {"foo"=>:bar} permitted: >
params.permitted?
# => nil

Rails 5.1

It is initialized opens a new window 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. opens a new window

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 opens a new window 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 opens a new window 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! opens a new window

Get the book