Why You Need Strong Parameters in Rails
In 2012, GitHub was compromised by Mass Assignment vulnerability. A GitHub user used mass assignment that gave him administrator privileges to none other than the Ruby on Rails project. In this post, I will explain this vulnerability and how you can use the Rails strong parameters API to address it.
This is the fifth post in my series on Rails Controllers. Originally published last year, I have revised it and have included the new expect method introduced in Rails 8.
As Rails developers, we are used to using post_params or user_params methods in our controller classes. Until recently, I had no idea why we were doing this, instead of using the params hash directly, and, as a Reddit commenter correctly pointed out, they often felt like a magic ritual that purged out the evil spirits from the parameters.
Turns out, strong parameters are there for a good reason, as a solution to a common security vulnerability. This post will cover the why, what, and how of strong parameters in Rails and how they protect against this vulnerability.
Mass Assignment Vulnerability
What is it? Mass Assignment means unintentionally assigning values to multiple variable or object properties at a time.
Why is it bad? Consider the following object:
user = {
name: "Jason",
location: "Chicago",
admin: false
}Now let’s say we want to update the user's location to "San Diego". The standard 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
location: "San Diego",
}
user.update!(params)Boom! You are done. But wait, the next day, you realize that the user now has 'admin' access - which you didn't want.
How did that happen?
The answer is the other properties in the params object above that I didn’t list. Here’s the complete form data submitted by the user:
params = {
name: "Jason",
location: "San Diego",
admin: true
}Because the software updated all the properties of the user using the params hash in its entirety, the admin property was updated to true. As a result, Jason is now an admin.
Now, this is obviously a dumbed-down example to show the problem. Of course, the web application is not foolish enough to ask any random user if they are an admin, on a form that anyone can submit.
But I hope you got the idea. It’s terrible to update all the properties of an object from a source that we don’t trust!
💡 Mass assignment vulnerability occurs when your application allows user input to be assigned to multiple object attributes or database fields at once without proper restrictions, potentially leading to unauthorized data modification.
The above example showed an update operation, but the same principle applies when creating new records. You should never blindly use user-submitted data to create objects. Instead, ensure that only the permitted attributes are used, preventing mass assignment vulnerabilities and unauthorized changes.
# don't do this
user = User.create(params[:user])
Github is Hacked
If you want to see a real-world example, in 2012 GitHub was compromised 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 Preston-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 Protect Against Mass Assignment?
The solution is simple. To start with, you should at least define which model attributes you want to make mass assignable. Before you update any object, filter out only the properties you want and nothing else!
Here's a simple and manual way of doing this, before Rails had strong parameters.
class UsersController < ActionController::Base
def create
User.create(user_params)
end
def update
User.find(params[:id]).update(user_params)
end
private
def user_params
params[:user].slice(:name, :location)
end
end
This pattern was so common that Rails released a plugin as well as a gem for it (see strong_parameters), and later versions of Rails (v4 onwards) provide it out of the box. Here’s how it works.
How Strong Parameters Work in Rails
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. As the documentation says, this class allows you to choose which attributes should be permitted for mass updating and thus prevent accidentally exposing that which shouldn’t be exposed.
params.require(:user).permit(:name, :location)
In the above code, we explicitly mark the user parameter as required using the require method, and only permit the name and location parameters inside the user. If the user 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({
user: {
name: "Jason",
location: "Chicago",
admin: false
},
client: {
name: "David"
}
})
=> #<ActionController::Parameters {"user"=>{"name"=>"Jason", ..}, "client"=>...} 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 only the user parameter as required, only permitting the name and location parameters.
user_params = params.require(:user).permit(:name, :location)
=> #<ActionController::Parameters {"name"=>"Jason", "location"=>"Chicago"} permitted: true>
Note that after validating the parameters and allowing only the attributes we want (name and location, and not admin), 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(:user).permit!
💡 Remember that the strong parameters are only validated when you try to use them to update the active model’s properties. It won’t prevent you from accessing the data.
> params
=> #<ActionController::Parameters {"user"=>#<ActionController::Parameters {"name"=>"Jason", "location"=>"Chicago", "admin"=>false} permitted: false>, "client"=>{"name"=>"David"}} permitted: false>
# This works
params[:client]
=> #<ActionController::Parameters {"name"=>"David"} permitted: false>
params[:client][:name]
=> "David"
# This doesn't
User.update!(params[:client])
So far so good. Now let's explore a new method Rails added very recently to simplify the strong parameters API.
Rails 8 Introduces expect
Until now, the standard approach to using strong parameters in Rails was through the require and permit methods. However, with the release of Rails 8, a new expect method was added on the ActionController::Parameters object.
The expect method lets you safely permit and require parameters in one step, and it reads well, too!
user = { name: "Jason", location: "Chicago", admin: false }
params = ActionController::Parameters.new({
user: {
name: "Jason",
location: "San Diego",
admin: true
}
})
user_params = params.expect(user: [:name, :location])
user_params # => #<ActionController::Parameters {"name"=>"Jason", "location"=>"San Diego"} permitted: true>
user.update!(user_params)
# => #<Person id: 1, name: "Jason", location: "San Diego", admin: false>Based on the latest Rails guides, it seems that expect is now the preferred way to handle strong parameters moving forward. I recommend reading the pull request that introduced this method to understand the reasoning behind the expect method.
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💡 Note: If you permit a key pointing to a hash, Rails doesn't allow the entire hash. You must specify the permitted hash attributes.
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.
ActionController::Parameters.new
=> #<ActionController::Parameters {} permitted: false>
ActionController::Parameters.permit_all_parameters = true
=> true
ActionController::Parameters.new
=> #<ActionController::Parameters {} permitted: true>
This is, of course, not recommended.
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:
falseto do nothing and continue as if nothing happened.:logto log an event at theDEBUGlevel.:raiseto raise anActionController::UnpermittedParametersexception.
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)
Sign up for my newsletter
Let's learn to become better developers.
Comments (4)
Hi Akshay, I really enjoy all your articles because they explain the reasons behind why we need them. I wasn't aware of the mass assignment issue with GitHub, so it's good to learn about that. It adds clarity to why strong parameters are necessary.
Thank you for the comment, Chaitali. Nice to hear that you found the post helpful :)
Hi and happy new year to you! It's always a pleasure to read your articles. I was reading this one about strong parameters, specifically looking for their relationship with nested attributes, how to properly name nested params for strong params to work smoothly, etc. Have you already explored that topic in one of your posts? Thanks Christian
Thanks, Christian. Happy new year to you as well, and glad you found my writing helpful. Good question! I don't think I've written about how strong parameters work with nested params, but sounds like an excellent topic to explore. I might re-publish this post, updated with the nested parameters. Stay tuned!