You’ve used strong parameters in your Rails applications, but did you know what problem they are solving? I didn’t. So did some reading and learned about a common security vulnerability. In this post, I will explain the Mass Assignment vulnerability and how you can use the Rails strong parameters API to address it.
Mass Assignment Vulnerability
What is it? Mass Assignment means assigning values to multiple variable or object properties at a time.
Why is it bad?
Consider the following object:
user = {
name: "Jason",
company: "37signals",
owner: true
}
Now let’s say we want to update the company’s name to “Basecamp”. The typical web-application way to do this would be to receive some data, preferably from a form submission from the user, and use that data to update our user’s properties.
params = {
# some data
company: "Basecamp",
# some data
}
user.update!(params)
Boom! You are done. But wait, the next day, Jason comes to the office and realizes there’s a new owner. He’s not listed as the owner anymore of the company.
How did that happen?
The answer is the other properties in the params
object above that I didn’t list. Here’s what the complete object looks like:
params = {
name: "Jason",
company: "Basecamp",
owner: false
}
Because the software updated all the properties of the user
using the params
hash in its entirety, the owner
property was updated to false
. As a result, Jason is not an owner anymore.
Now, of course, this is a very simple example to illustrate the problem. Of course, the web application is not foolish enough to ask whether the owner is indeed the owner on a front-end form that anyone can submit, and thankfully, Jason is still the owner.
But you get the idea. It’s terrible to update the properties of an object from a source that we don’t trust!
Mass assignment vulnerability occurs when your application assigns the data from a user input to multiple objects, variables, of database fields at once. Updating the object’s properties allows an attacker to modify or overwrite the existing data.
If you want to see a real-world example, in 2012 GitHub was compromized by this vulnerability. A GitHub user used mass assignment that gave him administrator privileges to none other than the Ruby on Rails project. As GitHub co-founder, Tom Preson-Werner later said,
“The root cause of the vulnerability was a failure to properly check incoming form parameters, a problem known as the mass-assignment vulnerability,”
How to prevent it?
The solution is simple. Before you update any object, filter out only the properties you want and nothing else!
class UsersController < ActionController::Base
def create
User.create(user_params)
end
def update
User.find(params[:id]).update_attributes!(user_params)
end
private
def user_params
params[:user].slice(:name, :company)
end
end
This pattern was so common that Rails released a plugin as well as a gem for it (strong_parameters), and later versions of Rails (v4 onwards) provide it out of the box.
Here’s how it works.
How Strong Parameters Work
The philosophy behind strong parameters is “assume unsafe until proven otherwise“. In simple terms, Rails marks the parameters forbidden to be used in mass assignment until you explicitly mark them as safe.
How do you mark parameters as safe?
Using the require
and permit
methods on the params
hash, which is an instance of ActionController::Parameters
.
params.require(:client).permit(:name, :company)
In the above code, we explicitly mark the client
parameter as required using the require
method, and only permit the name
and company
parameters inside the client
. If the client
parameter has an admin
parameter, Rails won’t allow you to use it in a mass assignment operation.
Let’s open the Rails console by running bin/rails console
and run some experiments:
params = ActionController::Parameters.new({
client: {
name: "Jason",
company: "Basecamp",
admin: true
},
user: {
name: "David"
}
})
=> #<ActionController::Parameters {"client"=>{"name"=>"Jason", ..}} permitted: false>
Yes, you can simply create a params
object on fly. You don’t need to make a request from the browser to access it. Pretty cool, right?
Notice that the output of the above code was an object with the permitted
property set to false
. Now, let’s mark the client
parameter as required, only permitting the name
and company
parameters.
client_params = params.require(:client).permit(:name, :company)
=> #<ActionController::Parameters {"name"=>"Jason", "company"=>"Basecamp"} permitted: true>
Note that after validating the parameters and allowing only the attributes we want, the result client_params
has the permitted
property set to true
. The client_params
parameter is now safe to use in a mass-assignment operation.
To permit an entire hash of parameters, use the permit!
method.
params.require(:client).permit!
> params
=> #<ActionController::Parameters {"client"=>#<ActionController::Parameters {"name"=>"Jason", "company"=>"Basecamp", "admin"=>true} permitted: false>, "user"=>{"name"=>"David"}} permitted: false>
# This works
params[:user]
=> #<ActionController::Parameters {"name"=>"David"} permitted: false>
params[:user][:name]
=> "David"
# This doesn't
User.update!(params[:user])
Nested Parameters
You can use strong parameters on nested params
, as shown below:
params = ActionController::Parameters.new({
product: {
name: "iPhone",
price: 500,
accessories: [{
name: "headphone",
price: 35
}, {
name: "adapter",
price: 90
}]
}
})
permitted = params.permit(product: [ :name, { accessories: :price } ])
permitted.permitted? # => true
permitted[:product][:name] # => "iPhone"
permitted[:product][:price] # => nil
permitted[:product][:accessories][0][:price] # => 35
permitted[:product][:accessories][0][:category] # => nil
Permit All Parameters
If you want to permit all parameters by default, Rails provides the permit_all_parameters
option, which is false
by default. If set to true
, it will permit all the parameters (not recommended).
ActionController::Parameters.new
=> #<ActionController::Parameters {} permitted: false>
ActionController::Parameters.permit_all_parameters = true
=> true
ActionController::Parameters.new
=> #<ActionController::Parameters {} permitted: true>
Take Specific Action for Unpermitted Parameters
You can also control the behavior when Rails finds the unpermitted parameters. For this, set the action_on_unpermitted_parameters
property, which can take one of three values:
false
to do nothing and continue as if nothing happened.:log
to log an event at theDEBUG
level.:raise
to raise anActionController::UnpermittedParameters
exception.
params = ActionController::Parameters.new(name: "DHH")
### false ###
ActionController::Parameters.action_on_unpermitted_parameters = false
params.permit(:rails)
=> #<ActionController::Parameters {} permitted: true>
### log ###
ActionController::Parameters.action_on_unpermitted_parameters = :log
params.permit(:rails)
2022-05-17 18:39:23.099411 D [56269:4120 subscriber.rb:149] Rails -- Unpermitted parameter: name
=> #<ActionController::Parameters {} permitted: true>
### raise ###
ActionController::Parameters.action_on_unpermitted_parameters = :raise
params.permit(:rails)
=> #../metal/strong_parameters.rb:1002:in `unpermitted_parameters!': found unpermitted parameter: :name (ActionController::UnpermittedParameters)
All right, that’s enough for today. I hope you now have a better understanding of why strong parameters exist in Rails and how you can use them to build safe and secure applications.
As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I get from developers, and I look forward to hearing from you.
If you'd like to receive future articles directly in your email, please subscribe to my blog. If you're already a subscriber, thank you.