26: Hackers Love Mass Assignment 

(view original Railscast)

Other translations: It

Mass assignment is something most Rails programmers make use of as it provides an easy way to populate the properties of a model object from a form. Unfortunately its simplicity can make it a target for hackers. We’ll explain how and show you how to solve it in this episode.

Our simple registration form.

Above is a simple user registration form. When a user enters their user name and clicks “submit” a new User model is created. Our user model is defined in the schema like this, with a string field called name and a boolean field called admin that defaults to false.

create_table :users do |t|
  t.string :name
  t.boolean :admin, :default => false, :null => false

When a user fills in the form and clicks register, the create action is called.

def create
  @user = User.new(params[:user])
  if @user.save
    flash[:notice] = "Successfully registered"
    redirect_to :action => ’show’, :id => @user.id
    render :action => :new

When a user is created, the new User is populated from the params hash which, as discussed in the previous episode, should not be trusted. We are passing all of the values in the params[:user] hash and setting them as attributes of our new user. We’ll show now how this can be compromised by a malicious hacker to make himself an admin.

Hacking The Site

A command-line tool like curl can be used to pass POST parameters to a page.

curl -d "user[name]=hacker&user[admin]=1" http://localhost:3000/Users/

In the line above we’ve sent a name of ‘hacker’ and set admin to true. If we look in our development log we can see that new user has become an admin.

Processing UsersController#create (for at 2009-02-03 20:18:54) [POST]
  Session ID: 8daeaad6eb382c903e595e704b626ef7
  Parameters: {"user"=>{"name"=>"hacker", "admin"=>"1"}, "action"=>"create", "controller"=>"users"}
  SQL (0.000390) INSERT INTO users ("name", "admin") VALUES(’hacker’, ’t’)
Redirected to http://localhost:3000/users/show/2

The hacker was able to do this because of mass assignment: we’re creating a new user using the parameters supplied in the params hash and the hacker has supplied an admin parameter which has been passed through. The hacker has therefore made himself an admin.

Protecting Attributes

The way to stop this kind of attack is to limit the attributes that can be set from the form (or from a POST method). We do this in the model, in this case our User model. Rails has a method called attr_protected which allows you to define which of a method’s attributes can’t be set via mass assignment.

class User < ActiveRecord::Base
  has_many :comments
  attr_protected :admin

Our User model with a protected admin attribute.

Now, if we send our curl command again and look in the development log then we can see that the admin attribute hasn’t been set to true, even though it was supplied as true in the params hash.

Processing UsersController#create (for at 2009-02-03 20:37:49) [POST]
  Session ID: 381cee077c1367bf0cc410a2259adb96
  Parameters: {"user"=>{"name"=>"hacker", "admin"=>"1"}, "action"=>"create", "controller"=>"users"}
  SQL (0.000327)   INSERT INTO users ("name", "admin") VALUES(’hacker’, ’f’)
Redirected to http://localhost:3000/users/show/5

The admin attribute is now set from the params hash.

Not Quite There Yet

We’ve now stopped that method of hacking, but there’s still a hole in our site. There is a relationship in our application so that a user has many comments and has_many provides a way of setting the comment_ids via mass assignment. We use curl again to hack the comment ids.

curl -d "user[name]=hacker&user[admin]=1&user[comment_ids][]=1&user[comment_ids]=2"

Gaining control of comments that aren’t ours.

The has_many relationship between User and Comment gives the User model a comment_ids method that takes an array of comment ids. We’ve hacked the parameters hash above to claim ownership of the comments with ids 1 and 2. In the development log we can see that our hacker now owns those comments.

Processing UsersController#create (for at 2009-02-04 20:27:36) [POST]
Session ID: e6bee21260899c7dce47bc5040dcd467
Parameters: {"user"=>{"name"=>"hacker", "comment_ids"=>["1", "2"], "admin"=>"1"}, "action"=>"create", "controller"=>"users"}
Comment Load (0.001) SELECT * FROM comments WHERE (comments."id" IN (1,2)) 
SQL (0.001) INSERT INTO users ("name", "admin") VALUES(’hacker’, ’f’)
  Comment Update (0.000094)   UPDATE comments SET "title" = ’Comment 1’, "user_id" = 8 WHERE "id" = 1
  Comment Update (0.000071)   UPDATE comments SET "title" = ’Comment 2’, "user_id" = 8 WHERE "id" = 2

Our hacker now owns comments that aren’t his.

To stop this happening it’s better to use attr_accessible in our model instead of attr_protected. Now we’re explicitly saying which attributes can be modified by mass assignment. We’ll update our User model to just allow updates to the name attribute.

class User < ActiveRecord::Base
  has_many :comments
  attr_accessible :name

A last look in the development log shows that our hacker hasn’t got admin privileges and hasn’t got ownership of any comments that aren’t his.

Processing UsersController#create (for at 2009-02-04 20:39:15) [POST]
  Session ID: 48b9264e8da94d0a0edadce5e31ac500
  Parameters: {"user"=>{"name"=>"hacker", "comment_ids"=>["1", "2"], "admin"=>"1"}, "action"=>"create", "controller"=>"users"}
  SQL (0.000307)   INSERT INTO users ("name", "admin") VALUES(’hacker’, ’f’)
Redirected to http://localhost:3000/users/show/9

We’ve seen in this episode that it’s easy for unprotected models to have their attributes manipulated by malicious users. Use of attr_accessible can stop these attacks and keep your models safe.