Strong(ly Typed) Parameters
Why we’re forking the Strong Parameters gem.
Mass Assignment: Not Just for Higgs Bosons
In one of our Ruby on Rails sites, a new user can activate herself by filling out a large web form full of properties that correspond to model attributes. This is submitted to the controller, which then adds those properties to the user with this code:
@user.update_attributes(params[:user]). Of course, there are some attributes the user isn’t allowed to set for herself, such as the boolean property admin. There’s no checkbox for this attribute in the form. But what happens if the user manually adds such a checkbox, and checks it?
Well, nothing happens, because we saw this coming. Our User model contains a whitelist of attributes that can be “mass-assigned”. When a method like
update_attributes is called, Rails looks for a line like
attr_accessible :admin in the model, and silently ignores that attribute if it doesn’t find it. Sidenote: Rails also allows a blacklist approach (certain attributes are explicitly protected, everything else is fair game), but it’s a general principle that whitelists are more secure. If you fail to add something to a whitelist, you get an error. If you fail to add something to a blacklist, you get an exploit.
Mass assignment protection using
attr_accessible applies everywhere in your code. If you want to make a user an admin in some other method, or in test code, you need to use a method like, well, the singular
update_attribute. Not only can this be annoying, it can lead to overly-inclusive whitelists! Developers who want attributes A,B,C to be assignable using one method, and B,C, and D to be assignable using another method, will often union the two and whitelist A,B,C, and D in the model. If the two methods have different authorization levels, this constitutes a subtle vulnerability. Can we do better?
Strong Parameters: We Can Do Better
Strong Parameters is built into Rails 4, and exists as a backport gem for Rails 3 applications. It allows you to whitelist accepted attributes at a better granularity level: in individual controller methods, rather than the model.
With Strong Parameters, rather than pass the untrusted params Hash directly, we first extract the attributes we expect to find:
params.permit(user: [:first_name, :last_name])[:user]
We can then call update_attributes on the result, secure in the knowledge that only the whitelisted keys are there. This is an excellent opportunity to DRY up your parameter parsing. For example, if several methods in your controller contain the line
@user = User.find_by_uuid(params[:uuid]), you can refactor to:
def update @user = user_from_params end private def user_from_params User.find(params.permit(:uuid)) end
before_filter: set_user private def set_user @user = User.find(params.permit(:uuid)) end
You can also make these methods “noisier” by using require instead of permit, and/or defining an action to take when an unexpected parameter is there.
This is a much more powerful tool. It allows us to use mass assignment freely when the input is trusted, and protects against exploits like CVE-2012-2695, where an unexpected parameter that isn’t a model attribute, but still affects code, is included in the request Hash. But there are still some exploits it does not address.
Strangely Typed Parameters
Let’s consider the code above that retrieves a user by uuid. What happens if the uuid parameter is there, but is an integer rather than a string? This is possible when the request body is in JSON or XML, formats that allow types to be specified instead of simply parsing everything as either a string or a nested Hash.
The answer to this rhetorical question depends on your database. Many databases will, perversely, typecast the data to match the search rather than vice versa, and most strings will be cast to 0. So the first user will be returned. If you were counting on that user’s UUID being secret, you’re in trouble.
Or suppose the uuid parameter is the array
[nil]. At one point, this would generate the SQL
select * from User where uuid in (NULL), which could find an internal system user.
Rails’s typical approach when these sort of exploits are discovered is to patch ActiveRecord’s SQL generation. But that doesn’t protect against the next strangely typed parameter exploit, the one we haven’t thought of yet, and it doesn’t protect against other Ruby code making unwarranted assumptions about the type of untrusted data.
The Strong(ly Typed) Parameters gem: Part of my ongoing plot to turn Rails into Java
Our current approach is to have our Rails services use this fork of the Strong Parameters gem. The fork behaves similarly, except it ensures that every parameter passed is a string, as Rails applications almost always expect parameters to be. When you don’t want a string, you can simply declare the type you do want.
params.permit(as_xml: Boolean) => permits true or false params.permit(pages: [Numeric]) => permits an array of numbers
Any class or module can be used here.
The one exception is when the parameter being passed is a Hash that shares a name with one of your ActiveRecord models (or something that exposes a similar columns method, since duck typing isn’t always evil). In this case, the default assumption is that the expected type of the values corresponds to that model’s attribute’s types. So if you do
params.permit(user: [:id, :first_name, :last_name])
Strong Parameters will expect a hash containing an id Fixnum and two strings. You can override this, naturally, by changing it to
params.permit(user: [:id => String, :first_name, :last_name])
We can make it easiest to spot unsafe parameter parsing by banishing it from the public controller methods. The params Hash should only appear in private controller methods, where it should be accessed only through its permit and require methods. This helps ensure that parameters are treated in a consistent way.
Reaping the Rewards
I’d already written up this blog post, when, coincidentally, this Rails security alert went out, warning of a potential exploit when a hash is passed in where another type is expected. This is exactly the sort of attack that Strongly Typed Parameters was designed to foil, and indeed it provably does.
Patches welcome. Alternate solutions that are equally or more comprehensive welcome. And, as always,