Thu Apr 5 15:25:15 UTC 2012

A worst practice in Ruby on Rails

A pretty common coding pattern in Ruby on Rails is the following:

app/controller/users_controller.rb:

  def update
    @user = User.find(params[:id])
    params[:user].delete(:admin) # make sure to protect admin flag
    respond_to do |format|
      if @user.update_attributes(params[:user])
      [...]
One would assume here that params[:user].delete(:admin) would not allow the admin flag to be assigned.
This is a bad coding practice I've seen in various RoR apps in the wild. Instead attr_protected / attr_accessible should be used.

So, how would you exploit this controller to assign the admin flag despite is being deleted?

Trick 1: Mass assignment magic to the help

Observe the following Ruyb on Rails code:

rails/activerecord/lib/active_record/attribute_assignment.rb:
   
01    def assign_attributes(new_attributes, options = {})
02      return unless new_attributes
03
04      attributes = new_attributes.stringify_keys
05      multi_parameter_attributes = []
06      nested_parameter_attributes = []
07      @mass_assignment_options = options
08
09      unless options[:without_protection]
10        attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
11      end
12
13      attributes.each do |k, v|
14        if k.include?("(")
15          multi_parameter_attributes << [ k, v ]
16        elsif respond_to?("#{k}=")
17          if v.is_a?(Hash)
18            nested_parameter_attributes << [ k, v ]
19          else
20            send("#{k}=", v)
21          end
22        else
23          raise(UnknownAttributeError, "unknown attribute: #{k}")
24        end
25      end 
26
27      # assign any deferred nested attributes after the base attributes have been set
28      nested_parameter_attributes.each do |k,v|
29        send("#{k}=", v)
30      end
31
32      @mass_assignment_options = nil
33      assign_multiparameter_attributes(multi_parameter_attributes)
34    end
So if the model does not use the build in mass assignment protection (attr_protected/attr_accessible),
the code flow would allow to call any public method ending in "=". In line 16 the code checks for a public
mehtod for each attribute key. Subsequently in line 29 it calls the method with the value Hash of the key as argument.

How does this help us?
See further in the same piece of ActiveRecord code:
def attributes=(new_attributes)
  return unless new_attributes.is_a?(Hash)

  assign_attributes(new_attributes)
end

\o/ attributes= ends in =, and is callable via assign_attributes. So if we give the admin attribute like:

user[attributes][admin]=1 it won't be deleted by params[:user].delete(:admin) due to being params[:user][:attributes]

This issue is briefly described (without elaborating how to exploit this) here

Trick 2: Multiparameter attributes

Another way to circumvent the above deletion of the admin flag would be so called "multiparameter attributes". Those work as follows:
If you want to give multiple parameters to one attribute it's encoded in Ruby on Rails like:
 user[name(1)]=first_name&user[name(2)]=last_name

So here the parameter "name" gets two attributes "first_name" and "last_name".
Now we can do the following to exploit the above controller:

user[admin(1)]=true

Here we give only one parameter as a multiparameter which is completely legal, as the parameter is now "admin(1)" the user.delete(:admin) statement won't catch it. :)
Credit: @meder came up with this 2nd approach on the <spam>RailsFun mailinglist</spam>

Posted by Joernchen | Permanent link