Let’s say we’ve created a simple profile page to allow our users to update their e-mail address, locale, time zone, and various other settings. This profile page exposes only those model attributes that we want the user to be able to update. Other attributes on the model should not be updateable by the user.
We opt to surface the updateable attributes on the profile page and assume that the user won’t be able to update the remaining attributes since they won’t even appear on the page. But does that really provide enough protection?
Our model and database schema are defined as follows:
class User < ActiveRecord::Base end create_table :user |t| t.column :username, :string t.column :password, :string t.column :fname, :string t.column :lname, :string t.column :email, :string t.column :permission, :string end
Our model can store a username, password, first and last name, e-mail address, and permission level. The permission level is used to determine what the user is allowed to do on the site. A permission level might be "Moderator" or "Admin."
Our profile form surfaces the attributes that we want to be updateable:
<%= start_form_tag %> <%= text_field :user, :fname %> First Name
<%= text_field :user, :lname %> Last Name
<%= text_field :user, :email %> E-mail Address
<%= submit_tag %> <%= end_form_tag %>
It's obvious that we only want the user to update his first and last name and e-mail address. Now let's examine the action being called when we submit the profile form:
def update @user = User.find(params[:id]) if @user.update_attributes(params[:user]) redirect_to :action => "success" end end
Our action looks up a user, updates its attributes with the new values passed on the request, and redirects to a new action if the update was successful. Simple enough, yet the code above could allow a malicious user to update the permission level of his account... something we certainly don't want him doing! Assuming the action is not protected against GET requests, we could simply visit the following URL in our browser to give the user a different permission level:
Protecting the action against GET requests would prevent the attack from occuring via a URL, but the attacker could easily generate a POST request through other means. Rails provides a way to protect against such attacks: the #attr_protected class method:
class User < ActiveRecord::Base attr_protected :permission end
Now if we attempt our attack again, the permission level simply doesn't get updated. The other attributes sent on the request are assigned correctly, but the permission level isn't. Why not?
It turns out that #attr_protected makes it impossible to update the permission attribute through the #update_attributes method (or any other mass assignment method for that matter). The only way we can update the permission level now is by using single-assignment techniques such as:
It's generally a good idea to protect attributes that could cause a security breach in your system. These might include usernames, passwords, software keys, and even product prices. While by no means a silver bullet, #attr_protected is still a good tool to use as you begin locking down sensitive parts of your application.
#attr_protected accepts multiple attributes so we could rewrite our model again as:
class User < ActiveRecord::Base attr_protected :username, :password, :permission end
#attr_protected also has a more conservative cousin, #attr_accessible. It renders all attributes on the model unavailable for mass assignment unless they are specifically named. This differs from #attr_protected which lets you start in an "all-open" state and only restrict attributes as necessary. Both methods are handy to have in your toolbox.