How do strong params work?

September 24, 2024

If I had to list the Rails features I used most regularly but understood the least, strong parameters would rank near the top. They're one of the first concepts you encounter when learning Rails. At that time, you're usually happy, when you understand what you need to do, and spend little to no time on why things work the way they do. This article delves into what strong parameters are and, more importantly, how they function.

What are strong params?

Strong parameters are a feature in Ruby on Rails designed to simplify controller code, handle errors for missing parameters, and enhance security by mitigating mass assignment vulnerabilities.

How can you use strong parameters?

Here's an example of how strong parameters are usually used in Rails controllers:

class PostsController < ApplicationController
    # Other actions

    def create
        @post = Post.new(post_params)

        if @post.save
            redirect_to @post
        else
            render 'new', status: :unprocessable_entity
        end
    end

    def update
        Post.find(params[:id])

        if @post.update(post_params)
            redirect_to @post
        else
            render 'edit', status: :unprocessable_entity
        end
    end

    # other actions

    private

    def post_params
        params.require(:post).permit(:title, :content)
    end
end

First you require the object parameter matching the model, and then you permit the parameters you accept for mass assignment. In this example we require a post object and only permit the title and content params.
Let's explore what require and permit do respectively.

How does Rails handle parameters?

When receiving a request Rails controllers create an ActionController::Parameters instance with the request parameters. Let's simulate that:

params = ActionController::Parameters.new({post: { title: 'Title', content: 'Content' }})
#<ActionController::Parameters {"post"=>{"title"=>"Title", "content"=>"Content"}} permitted: false>

An ActionController::Parameters instance is similar to a hash and has access to many hash like methods. However, it also contains specific methods to manage which keys are permitted.

What does require do?

Requiring a parameter returns the value of that parameter:

params = ActionController::Parameters.new({post: { title: 'Title', content: 'Content' }}).require(:post)
#<ActionController::Parameters {"title"=>"Title", "content"=>"Content"} permitted: false>

As you can see, require does not change the permitted status of the parameters; the parameters are still marked as permitted: false.

But what happens, if the parameter doesn't exist?

params = ActionController::Parameters.new({title: 'Title', content: 'Content' }).require(:post)
#eval error: param is missing or the value is empty: post

It raises an ActionController::ParameterMissing error. The same happens, when the param is empty:

params = ActionController::Parameters.new({post: {}}).require(:post)
#eval error: param is missing or the value is empty: post

This makes sure that the necessary data is available to prevent nil errors in controller actions. On the other hand it prevents processing irrelevant or harmful data.

How does require work under the hood?

In order to see how require prevents nil errors, let's have a look at the Rails source code:

# actionpack/lib/action_controller/metal/strong_parameters.rb
def require(key)
  return key.map { |k| require(k) } if key.is_a?(Array)
  value = self[key]
  if value.present? || value == false
    value
  else
    raise ParameterMissing.new(key, @parameters.keys)
  end
end

If the provided key is an array it runs require for every element of the array. For non-array keys, it raises a ParameterMissing error if the value is empty, except for false. Otherwise, it returns the value associated with the key, still wrapped within an ActionController::Parameters instance. However, in some cases you might not want to raise an error, e.g. if you want to use the params in a new action. In those cases instead of .require you can use .fetch, which lets you set a default empty hash:

params.fetch(:post, {}).permit(:title, :content)

What does permit do?

To see what permit does let's first create a new ActionController::Parameters instance:

params = ActionController::Parameters.new({post: { title: 'Title', content: 'Content' }})
#<ActionController::Parameters {"post"=>{"title"=>"Title", "content"=>"Content"}} permitted: false>

Now let's try to assign the params to a new Post instance:

Post.new(params[:post])
#eval error: ActiveModel::ForbiddenAttributesError

As you can see it raises an ActiveModel::ForbiddenAttributesError preventing us from assigning unpermitted parameters. As we learned using require won't prevent this error:

Post.new(params.require(:post))
#eval error: ActiveModel::ForbiddenAttributesError

However, if we require :post and permit the parameters, we can assign them:

Post.new(params.require(:post).permit(:title, :content))
#<Post:0x0000000125213d58 id: nil, title: "Title", content: "Content", created_at: nil, updated_at: nil>

How do strong params work under the hood?

Let's explore the permit source code to get a better understanding of the mechanism.

def permit(*filters)
  params = self.class.new

  filters.flatten.each do |filter|
    case filter
    when Symbol, String
      permitted_scalar_filter(params, filter)
    when Hash
      hash_filter(params, filter)
    end
  end

  # other code

  params.permit!
end

First, a new ActionController::Parameters instance is created and stored in params. Then it flattens filters and iterates over each filter (every key you want to permit). Symbols and Strings are handled as scalar values via permitted_scalar_filter, which copies the parameter from the original parameters to the new params.
Hashes represent nested parameters. They are handled by hash_filter, which recursively adds parameters from the original parameters to the new params according to the hash structure.
If an action_on_unpermitted_parameters is set to :log or :raise, the unpermitted_parameters! method checks for and handles any unpermitted parameters according to the configured action. By default action_on_unpermitted_parameters is set to nil.
Finally, the permit! method is called on the new params object, marking all its values as permitted. The method then returns params, and thus, all values in it are permitted for mass assignment.

Conclusion

Starting to delve into the Rails source code can be daunting, but as we’ve seen, exploring strong parameters is an excellent starting point. Their role in increasing protection against mass assignment vulnerabilities is evident and not overly complex, given their relatively standalone implementation in the Rails framework. Understanding how strong parameters function provides a solid foundation for understanding other key aspects of Rails, enhancing both security and application integrity.