#26 Hackers Love Mass Assignment (revised)
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
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
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
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
class User < ActiveRecord::Base has_many :projects attr_protected :admin end
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
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
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
# 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
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
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.