Security in Rails became a hot topic recently when a vulnerability found in Github allowed a user to gain commit access to the Rails repository. The developers at Github did a remarkable job of fixing this issue but we can learn from it and make sure that we don’t have the same mass assignment vulnerability in our own projects.

Exploiting Insecure Mass Assignment

First we’ll show you how this vulnerability is exploited. Below is a user profile page from a Rails application.

If we click the “Edit Profile” link we’ll be taken to another page where we can edit some of the user’s information. We want to play the role of a hacker so we’ll change this user’s name to “Hacker”.

A user can also become an admin, but our hacker isn’t currently one. If we look at the database schema for this application we’ll see that a user is marked as an admin through a simple boolean field in the users table.

When the user edits their profile there’s no field in the form for changing the admin field so we should be safe from malicious users who want to make themselves administrators, yes? Well, no, the user can send anything they want to the server and with most modern browsers offering developer tools which make it easy to alter the current page it’s easy to change the name of the form field so that instead of modifying the name field is modifies a different one. We’ll change the name of the text field so that it’s user[admin] instead of user[name].

If we enter a value of 1 in this field now and update the user they are now an administrator.

How is this possible? This issue is related to how the controller assigns attributes to the model. If we look at the update action, which is triggered when the form is submitted, we’ll see that it calls update_attributes.

This method passes all the attributes from the form into the User instance. This is known as mass assignment and while it’s convenient it means that a user can set any attribute on the User model that they want.

Protecting Our Application

There are many ways that we can protect ourselves from this vulnerability. One option is to modify the model and user attr_protected to define the attributes that we don’t want to be settable through mass assignment, in this case admin.

We use attr_protected to define a black list of attributes that can’t be set by mass assignment. We can test this now by reloading the edit user form, changing the name of the text field to user[admin] again then setting the value in the textbox to 0 and submitting the form. When we do this we get a MassAssignmentSecurity::Error as we’ve tried to modify a protected attribute.

A Better Approach

We’ve successfully protected our application from this attack now so using attr_protected seems to have worked in protecting the admin attribute. We shouldn’t really be protecting out models in this way, however, as it’s easy to miss attributes that need protecting. For example a user has many projects and this has_many association adds an attribute to User called project_ids. We can try changing the name of the text field to this and see if we can modify something we shouldn’t be able to this way. This field expects an array of values so we’ll need to add empty square brackets after the name to make it user[project_ids][].

If we enter the id of a project into the text field now and submit it we’ll see that we now own a project that used to be assigned to another user.

We’ve now successfully taken over someone else’s project and this should be enough to convince us not to use attr_protected. Instead we should use attr_accessible which behaves like a whitelist and lets us specify the attributes we want to be able to be set through mass assignment.

We’re now safe from malicious users setting the admin attribute, the project_ids or any attribute on User apart from name. To make our application secure we should go through all its models that are updated by mass assignment and use attr_accessible in each one to define the attributes that can be modified. It’s easy to miss a model when we do this and it would be nice if there was a way to ensure that it was set in every one. In recent versions of Rails there’s a commented-out line in the application.rb file called active_record.whitelist_attributes. Setting its value to true this will protect us from having models without attr_accessible set.

/config/application.rb

# Enforce whitelist mode for mass assignment.# This will create an empty whitelist of attributes available for mass-assignment for all models# in your app. As such, your models will need to explicitly whitelist or blacklist accessible# parameters by using an attr_accessible or attr_protected declaration.
config.active_record.whitelist_attributes = true

We’ll need to restart the server for this change to be picked up. Once we’ve done any model that doesn’t have attr_accessible set will not be able to use mass assignment. If we try creating a new Project now we’ll see an error message.

We haven’t set attr_accessible on Project and so now we can’t set its name attribute. To fix this we just need to add it.

We’ll need to test our application thoroughly now to make sure that each model has a call to attr_accessible.

There’s another configuration option related to this in recent version of Rails. In the development config file there’s an option called mass_assignment_sanitizer. By default this is set to strict which means that an exception is raised when mass assignment fails. Earlier versions of Rails would just ignore these errors and fail silently which made these issues hard to debug. This option is also set to strict in test mode but not in production. These errors fail silently in production which is generally the behaviour we want. The protection still happens but no exception will be raised if there’s an error.

Making attr_accessible Dynamic

There’s often a requirement to make attr_accessible dynamic depening upon the current user. For example we might want the admin field to be set through the form but only if the current user is an administrator. This was covered in episode 237 so if you need to do that take a look at that episode.