Here's an idea:

class Member < ActiveRecord::Base
    ProtectedAttributes = [:is_admin]

    def protect_attributes!
      @protect_attributes = true
    end

    def attributes=(attributes)
      if @protect_attributes
        attributes.symbolize_keys!
        attributes = attributes.delete_if {|key, value| ProtectedAttributes.include? key }
      end

      super
    end
end

enabling:

>> member = Member.new
>> member.protect_attributes!
>> member.update_attributes(:is_admin => 1)
>> member.is_admin
=> 0

Meaning you can use mass-assignment methods on the front end without worrying about users spoofing form posts.

Thoughts?

8 Responses to “Quick & Easy attribute protecting per ActiveRecord instance”

  1. Alex MacCaw Says:

    I would rather it was the other way round - that you called some method to enable mass assignment of protected attributes. At the moment, you could easily forget to call the method.

  2. Luke Francl Says:

    Wouldn't it be easier to use attrprotected :isadmin ?

    Or better yet (what I like to do) is make all the attributes protected, and then enable only the ones users can assign to with attr_accessible.

  3. Stephen Bartholomew Says:

    @luke: The trouble with that is that it attr_accessible or attr_protected protects assignment any time you need to update attributes - this means if you have an admin system, you'll have to manually get around the protection.

    It's common to create methods to toggle something like 'is_admin', but that can be overkill in some cases.

    Sometimes you just want to be able protect some attributes from updating by front end users.

  4. Bob Zoller Says:

    I dig the idea, and have certainly felt the same pain before. I'm a whitelist sorta guy though, so I'd rather have to make a call to *un*protect, rather than remember to protect.

    What if the protection was enabled on a global basis, so you could ActiveRecord::Protection.enable! in an application-wide beforefilter, and then _mymodelinstance.unprotect! inside things like admin controllers?

    You'd have to think about non-web flows and make sure you've protected yourself there, but this makes it easy and those places are usually at less at risk anyway.

    You may be able to accomplish this with an aliasmethodchain on ActiveRecord::Base#attributes=

  5. Luke Francl Says:

    Stephen,

    In that case, I would use withunprotectedattributes:

    http://henrik.nyh.se/2008/02/withunprotectedattributes-rails-plugin

    Or possibly just bypass the protection with send:

    http://deaddeadgood.com/2008/6/25/how-to-bypass-attraccessible-and-attrprotected-in-rails

  6. Bertg Says:

    I think this is an other step in the wrong way.

    The concept of protected attributes to prevent HTTP post abuse is a violation of the MVC model. The controller is responsible to handle the data between the Model and the "views (or requests)".

    Something simple as: User.new(params[:user].slice(:name, :email)) will prevent any abuse. It will even prevent the modification of unprotected attributes that aren't supposed to be edited with a particular form.

  7. David Vrensk Says:

    My first though is that if you're going to do something like this, there's really no reason to modify the attributes hash. It was sent to you so that you could do something with it, not _to_ it. So start by dup:ing it.

    I can see that Bertg has a point too. Perhaps you could have a #protected_attributes= that you would use instead of #attributes= when you want to be on the safe side. That way the model doesn't need to have different states for protected and unprotected, and it would be pretty clear what you are trying to do. I don't think that #slice is the way to go, since it would be hard for a controller to know what the unprotected attributes are at all times. Or something like

    User.new(params[:user].slice(User.unprotected_attributes))
    
  8. Stephen Bartholomew Says:

    To be honest, I've actually re-thought this based on on what Bertg said.

    It's fairly trivial to just add a before_filter that you can apply to any actions that need to update a details you need to protect:

    def clean_member_params 
        if params[:user] 
            params[:user].slice!(:email_address, :password) 
        end 
    end
    

    I think the issue is that putting details that are meant to be used in context of a controller - i.e. with a logged in user - is not strictly kosher MVC.

Sorry, comments are closed for this article.