The Dangers of Direct Assignment in Rails with[:user])

Monday, April 30th, 2007

Although it is used in the vast majority of Ruby on Rails tutorials, putting data from a form directly into an object using a statement such as[:user]) can actually be an extreme security risk.

To understand why, it is necessary to first explain how this statement works. Forms generated by Rails’ default form helpers look something like this:

Username: <input type="text" name="user[username]" />
Password: <input type="password" name="user[password]" />

The user[name] format is automatically converted by rails into a hash in the params variable, which would contain the following if this form were submitted:

params[:user] = {
    :username => "Bob"
    :password => "Secret"

Since accepts a hash of key/value pairs that will become the initial values of all the attributes of the returned object,[:user]) is simply taking the hash of values passed in from the HTML form for the user, and directly setting them in the new object. The simplicity of this operation is also its downfall. For instance, a user could modify your registration form (either using a browser plugin our by downloading it to their computer) to the following:

Username: <input type="text" name="user[username]" />
Password: <input type="password" name="user[password]" />
<input type="hidden" name="user[admin]" value="1" />

If your user model used a boolean attribute called admin to specify whether a user had admin privileges, when the form is submitted your code would unwittingly grant the user admin access. Because params[:user][:admin] was set to 1 in the hash passed in from the form, it is automatically set to 1 in the user object and saved to the database.

The Solution

Fortunately, this security hole is easy to fix using either of two simple statements provided by ActiveRecord in your model code. The first is attr_protected, which can be used thus:

class User < ActiveRecord::Base
    attr_protected :admin

This simple statement, which can accept multiple parameters, sets up a blacklist of attributes that cannot be set from the hash passed to, effectively filtering the input and patching the security hole.

The second alternative, attr_accessible, uses a white-listing approach instead. Only those attributes explicitly given to attr_accessible can be set from the hash passed to, any others will be ignored.

class User < ActiveRecord::Base
    attr_accessible :username, :password

Which approach you use depends entirely on preference, whether you think you are more likely to forget to protect sensitive attributes or to allow access to non-sensitive ones. Whichever method you choose, protected attributes can always be set by calling their accessor method, such as user.admin = 1.

For more information on this topic, see the Ruby on Rails manual.

A Further Warning on Associations

According to this article, the mass assignment security hole also affects collections and associations, such as a User having many Articles. By specially crafting a form submission, a user could claim any article as their own, simply using something similar to:

<input type="hidden" name="user[article_ids][]" value="5" />

This is because rails automatically creates attribute accessors like article_ids for has_many associations, which accept an array of ids to associate the object with. The form snippet above would add a new id to that array, and give the user ownership/authorship of that article. The author of the above article makes the case that, because of this hole (and other possibly others opened up by associations and other rails magic), it is a better idea to explicitly white-list attributes using attr_accessible, rather than attr_protected. This way, something like article_ids, which most developers don’t know about (myself included), won’t open up your application to exploitation. Ignorance is not always bliss.

Although this methodology is somewhat more in the spirit of Python than Rails, it is undoubtedly the safest. Explicitly defining which variables can be passed in through a form is also the method taken by Formencode, a widely used validation library for Python.