237: Dynamic attr_accessible
Over three years ago episode 26 [watch, read] covered the subject of mass assignment and how it can cause security vulnerabilities. Rails 3 is now out and has a lot of security features enabled by default but this is one area that isn’t. Rails applications’ models still need to have their attributes protected against mass assignment in order to avoid malicious users updating them by sending POST requests to the server. If you aren’t familiar with this problem then take a look at episode 26, but in essence whenever you create or update a model in your controllers by using mass assignment you need to use
attr_accessible in the models to protect the attributes you don’r want to be updated otherwise users can set any attribute and this can lead to huge security problems.
It seems easy enough to add a call to
attr_accessible in every model but there are two potential problems when doing so. The first problem can occur when you’re testing your app. Sometimes you want to mass-assign attributes during testing and having models protected with
attr_accessible can make this difficult. One solution to this is to use factories, and this was covered in episode 158 [watch, read].
The second problem is that
attr_accessible is not dynamic. The attributes that are specified by it for a given model are hard-coded and if we want to change the attributes that are accessible based on, say, the user’s permissions that can be difficult to do. This was the case in Rails 2 but Rails 3 gives us a new way to make dynamic attributes and we’ll show you that in this episode.
Our Wiki Site
To demonstrate dynamic attributes we’ll use a wiki site. This site has has a number of articles and an article can be edited by anyone. On the edit form, along with the name and content fields is a checkbox field that allows a user to mark as an article as ‘important’.
When an article is marked as important its title will appear in red.
We’re going to modify the application so that only administrators can change the importance of articles; non-admin users should not be able to modify the
important field. It would be easy enough to modify the form to remove the checkbox so that it is only shown to administrators, but this wouldn’t solve our problem as it would still be possible for users to bypass this form and send a POST request that would modify the
important field for an article.
The solution to this problem lies in the controller and model layers, specifically the
update actions in the
ArticlesController as it’s here that the mass assignment happens. One approach we could take to protect the
important attribute would be to remove it from the parameter hash unless the current user is an admin.
def update params[:article].delete(:important) unless admin? @article = Article.find(params[:id]) if @article.update_attributes(params[:article]) flash[:notice] = "Successfully updated article." redirect_to @article else render :action => 'edit' end end
The trouble with this is that we have to remember to do it for every attribute we want to protect. Also there is no correlation with the
attr_accessible call in the model. It would be much better if we could instead make
We’ll take a look at the Rails API documentation to see if we can find anything about
attr_accessible that can help. An interesting thing to note in the documentation is that
attr_accessible is now included in the
ActiveModel::MassAssignmentSecurity module, not directly in ActiveRecord. This means that it can be included and used within any class and this makes it much more flexible. At the top of the documentation page is an example of
ActiveModel::MassAssignmentSecurity used directly within a controller instead of through a model, which is a really good idea. What’s really interesting though is a piece of code that shows us how to make
attr_accessible dynamic by overriding the
def mass_assignment_authorizer admin ? admin_accessible_attributes : super end
The code above changes the behaviour of the application based on whether the user is an admin or not which is exactly what we want to do. By overriding this method in our models we can change the fields that can be modified by mass assignment depending on any condition.
Article model currently looks like this:
class Article < ActiveRecord::Base attr_accessible :name, :content, :important end
This is a simple enough class with just an
attr_accessible call with three attributes. The
:important attribute is the one we want to make dynamic and we can do that by overriding
mass_assignment_authorizer in the class.
class Article < ActiveRecord::Base attr_accessible :name, :content private def mass_assignment_authorizer super + [:important] end end
By calling super in
mass_assignment_authorizer we’ll get the default behaviour which returns a whitelist sanitizer. You don’t need to be familiar with how this works to use it, you just need to know that you can add more attributes to it as we have done above. Once we’ve added this extra parameter here we can remove it from the list of parameters in
The changes we’ve mades so far won’t alter the behaviour of our application at all but we can make the accessibility of
:important dynamic now as it’s defined in an instance variable rather than at the class level. We’ll do this by adding a variable to the class that will contain a list of the attributes we want to be accessible.
class Article < ActiveRecord::Base attr_accessible :name, :content attr_accessor :accessible private def mass_assignment_authorizer super + (accessible || ) end end
Any parameters passed to
accessible will now be added to the list of accessible attributes and we can use this in our controllers. We’ll modify the update action so that it adds the
:important parameter only is the current user is an admin.
def update @article = Article.find(params[:id]) @article.accessible = [:important] if admin? if @article.update_attributes(params[:article]) flash[:notice] = "Successfully updated article." redirect_to @article else render :action => 'edit' end end
We can start up our application now to see if these changes work. If we log in to the application with an account that isn’t an administrator and edit an article, marking it as important, when we’re redirected back to the article’s page the headline is black indicating that the
important field hasn’t been changed.
If we make our account an administrator and edit the article again, this time the important field is updated and the headline changes colour.
Ideally admins should be able to edit any of the fields so it would be useful if accessible supported an
:all option that would allow us to easily make all of a model’s attributes editable. We can do this by modifying
def mass_assignment_authorizer if accessible == :all self.class.protected_attributes else super + (accessible || ) end end
The method now checks to see if
accessible is equal to
:all. If it is then we need to return something that will make all of the attributes modifiable. It would be good if we could just return an empty array, but unfortunately the object that is returned by
mass_assignment_authorizer is a sanitizer object so this won’t work. The way we’ve have worked around this is a little bit of a hack but it works well enough: we return
self.class.protected_attributes. This is used by the
MassAssignmentSecurity module to provide a blacklist of attributes that can’t be modified. As we’re not using
attr_protected in this class it will allow all attributes which is just what we want here. We can now modify the
ArticlesController to make all of the
Article’s attributes accessible by passing
def update @article = Article.find(params[:id]) @article.accessible = :all if admin? if @article.update_attributes(params[:article]) flash[:notice] = "Successfully updated article." redirect_to @article else render :action => 'edit' end end
If we test this out in the application we’ll see that admins can still edit the
In the controller we also need to apply the
accessible option to the
create action. If we just apply it like this then it will not work.
@article = Article.new(params[:article]) @article.accessible = :all if admin?
The reason that this doesn’t work is that the mass assignment happens in the
new call so by the time we’ve set
accessible it’s too late. We need to separate creating a new Article from assigning its attributes and slip the call to accessible in between the two.
def create @article = Article.new @article.accessible = :all if admin? @article.attributes = params[:article] if @article.save flash[:notice] = "Successfully created article." redirect_to @article else render :action => 'new' end end
You might want to make this behaviour more abstract and remove the duplication in the code in the two actions but this is dependent on how your permission system works so we’ll leave this up to you. One change we will make though is to extract the
mass_assignment_authorizer method out from the
Article model so that it can be used in all of the application’s models.
We’ll move the method into an initializer. In the
/config/initializers directory we’ll create a new file called
class ActiveRecord::Base attr_accessible attr_accessor :accessible private def mass_assignment_authorizer if accessible == :all self.class.protected_attributes else super + (accessible || ) end end end
This initializer modifies
ActiveRecord::Base so the behaviour will be applied to all models. Note that we still call
attr_accessible but with no arguments. This means that the default behaviour will be that no attributes can be set through mass assignment and that you’ll need to add another
attr_accessible call to each model to make attributes settable. We can now clean up the Article model to this:
class Article < ActiveRecord::Base attr_accessible :name, :content end
That’s it for this episode. We’ve now made
attr_accessible completely dynamic and can change its attributes based on a user’s permissions. What’s good about this approach is that everything is locked down by default and access is only granted where we explicitly specify it in the code. This makes mass assignment security issues a much smaller problem as by default all attributes are secure.