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.
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.
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.
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.
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.
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)
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>
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.
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.