#26 Hackers Love Mass Assignment (revised)
- Download:
- source codeProject Files in Zip (102 KB)
- mp4Full Size H.264 Video (14.3 MB)
- m4vSmaller H.264 Video (7.39 MB)
- webmFull Size VP8 Video (8.49 MB)
- ogvFull Size Theora Video (16.3 MB)
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.
ActiveRecord::Schema.define(:version => 20120308190940) do create_table "projects", :force => true do |t| t.integer "user_id" t.string "name" t.datetime "created_at", :null => false t.datetime "updated_at", :null => false end create_table "users", :force => true do |t| t.string "name" t.boolean "admin", :default => false, :null => false t.datetime "created_at", :null => false t.datetime "updated_at", :null => false end end
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
.
def update @user = User.find(params[:id]) if @user.update_attributes(params[:user]) redirect_to @user, notice: "Profile has been updated." else render "edit" end end
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
.
class User < ActiveRecord::Base has_many :projects attr_protected :admin end
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.
class User < ActiveRecord::Base has_many :projects attr_accessible :name end
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.
# 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.
class Project < ActiveRecord::Base belongs_to :user attr_accessible :name end
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.