Rails has always had a nice way of sanitizing user input coming from ubiquitous forms. Up until Rails 3, the solution was to list accessible fields right in your models. Then Rails 4 came along and introduced a different solution - strong_parameters
, allowing you to take a greater control over the sanitizing process. The following text should serve as a light introduction to the topic and also as a cookbook for solving more complex tasks.
Let’s start by taking a look at how strong_parameters
actually work. On a superficial level, every request wraps its params in ActionController::Parameters
, which allows you to whitelist specific keys.
If we try to use them directly without whitelisting in mass assignment, we’ll get ActiveModel::ForbiddenAttributesError
.
params = ActionController::Parameters.new(username: "john")
User.new(params)
# => ActiveModel::ForbiddenAttributesError
To make this work, we just need to whitelist the attributes we want in the mass assignment and Rails will be happy
params = ActionController::Parameters.new(username: "john")
> User.new(params.permit(:username))
We don’t have to whitelist all of the attributes though, but we’ll get a warning if we miss some of them
params = ActionController::Parameters.new(username: "john", password: "secret")
params.permit(:username)
# Unpermitted parameters: password
# => { "username" => "john" }
We can even chain them together and build up the filter, as every call to permit
will return an instance of ActionController::Parameters
params = ActionController::Parameters.new(username: "john", password: "secret")
params.permit(:username, :password).permit(:username)
# Unpermitted parameters: password
# => { "username" => "john" }
Using permit
won’t mind if the permitted attribute is missing
params = ActionController::Parameters.new(username: "john", password: "secret")
params.permit(:username, :password, :foobar)
# => { "username" => "john", "password" => "secret" }
Which can be good, but sometimes we do want to require a specific parameter to be present. We can do that by using require
instead of permit
, which will raise
params.require(:foobar)
# ActionController::ParameterMissing: param not found: foobar
There’s one thing different about require
, and that’s it returns the actual value of the parameter, unlike permit
which returns the actual hash.
params = ActionController::Parameters.new(username: "john")
params.permit(:username)
# => { "username" => "john" }
params.require(:username)
# => "john"
This becomes useful when we have nested parameters, as we almost always have when using forms, for example
params = ActionController::Parameters.new(user: { username: "john" })
params.require(:user)
# => { "username" => "john" }
params.require(:user).permit(:username)
# => { "username" => "john" }
You might be asking what’s the difference here, since they both return the same thing … well not exactly
params.require(:user).permitted?
# => false
params.require(:user).permit(:username).permitted?
# => true
This simply means that if you try to use the first case in mass assignment you’ll end up getting ActiveModel::ForbiddenAttributesError
.
With this knowledge, we can already convert our attr_accessible
to strong_parameters
in most of the cases.
# model
class User < ActiveRecord::Base
attr_accessible :username
end
# controller
def create
User.create!(params[:user])
end
Now let’s transform the code using strong_parameters
.
# model
class User < ActiveRecord::Base
end
# controller
def create
User.create!(params.require(:user).permit(:username))
end
If you are using any form of nested attributes, be it the Rails’ accepts_nested_attributes_for
or just simply sending along some nested form data, we need to tell strong_parameters
about it, as by default permit
only allows scalar values, no hashes or arrays.
Let’s start with the simpler of those two, arrays. If we simply use permit
on an attribute with array value, it will behave as if we didn’t permit it at all.
params = ActionController::Parameters.new(usernames: ["john", "kate"])
params.permit(:usernames)
# Unpermitted parameters: usernames
# => { }
To solve this we simply tell strong_parameters
that the key is an array
params = ActionController::Parameters.new(usernames: ["john", "kate"])
params.permit(usernames: [])
# => { "usernames" => ["john", "kate"] }
Doing this with hashes is very similar, we just have to name all of the keys contained in the hash
params = ActionController::Parameters.new(user: { username: "john" })
params.permit(user: [ :username ])
# => { "user" => {"username"=>"john"} }
As you can see this is different from using require
, since that will only return the nested hash and not the parent one.
params.require(:user).permit(:username)
# => { "username" => "john" }
There’s one last problem with this. We can’t easily permit nested attributes when we don’t know the exact key names. This can happen for example if you’re expecting custom JSON data which can be of any format and you’re storing it using the new Rails 4 JSON support in a PostgreSQL database.
params = ActionController::Parameters.new(user: { username: "john", data: { foo: "bar" } })
# let's assume we can't do this because the data hash can contain any kind of data
params.require(:user).permit(:username, data: [ :foo ])
# we need to use the power of ruby to do this "by hand"
params.require(:user).permit(:username).tap do |whitelisted|
whitelisted[:data] = params[:user][:data]
end
# Unpermitted parameters: data
# => { "username" => "john", "data" => {"foo"=>"bar"} }
There’s a GitHub issue for this but as mentioned in the issue comments this is by design and you can easily solve it using Ruby magic, as we showed above.
We hope this article will make it easier for you to get the hang of strong_parameters
and its sometimes not so obvious edge cases. Leave us a comment if you have any questions, tips or a missing use case. We’ll happily add it to the article.
We're building a tool to help businesses reach out to their customers more easily. It's called SendingBee and it's going to be awesome.
This is the blog of sensible.io, a web consultancy company providing expertise in Ruby and Javascript.