Strong Parameters in Rails

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.

7 min read
💡
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.

Add `Parameters#expect` to safely filter and require params by martinemde · Pull Request #51674 · rails/rails
Motivation / Background I&#39;ve been hunting around trying to fix the problem with the default, recommended way of handling parameters in Rails. user_params = params.require(:user).permit(:name, :…

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:

  • false to do nothing and continue as if nothing happened.
  • :log to log an event at the DEBUG level.
  • :raise to raise an ActionController::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)

That's a wrap. I hope you found this article helpful and you learned something new.

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. Your email is respected, never shared, rented, sold or spammed. If you're already a subscriber, thank you.