Hello,
Recently I was looking into upgrading one of our Rails 3.2 apps to use
strong_parameters. I encountered what seems like a flaw to me and I would like
to spark discussion about this, hoping for personal learning and potential
improvement of the rails framework.
The switch from protected attributes to strong parameters looks from the outside
like access control for mass assignment moves from the model to the controllers
(where, I agree, they belong). However looking at the implementation of strong
parameters, it seems easy for developers to accidentally introduce bugs.
How is that?
@params in a controller is of type ActiveController::Parameters, which inherits
from HashWithIndifferentAccess which in turn inherits from a Ruby Hash. Strong
Parameters extends ActiveController::Parameters to have a ‘permitted?’ function,
essentially acting as the whitelist of permitted parameters for mass assignment
[1].
Strong Parameters also overrides ActiveRecords sanitize_for_mass_assignment
function, where it checks if the attributes respond to ‘permitted?’. The
implementation assumes that if the attributes Hash does not respond to
‘permitted?’ the mass assignment decision should be deferred to ActiveRecord. If
it responds to ‘permitted?’ and the mass assigned attributes are not on the
whitelist it will raise an exception [2].
The ‘permitted?’ function here acts as a weird capability [3]. It is weird
because a capability that is not present will usually deny an action (mass
assignment) while here the action gets permitted in the absence of the
capability.
Why does that matter?
It matters because it is possible for a developer to accidentally lose that
capability accidentally very easily on the way from the controller (where
permit happened and the capability gets created) to the model (where the
capability gets used). This loss does happen silently and effectively disables
mass assignment protection.
How does that happen?
The only class aware of the ‘permitted?’ capability is
ActiveController::Parameters, if we call a method that is not aware of the
capability it can get lost as a side effect:
class SomeModel < ActiveRecord::Base
#fields :name, :protected_secret_field
include ActiveModel::ForbiddenAttributesProtection
end
#imagine a request w/ params = {‘name’ => 1, ‘protected_secret_field’ => 2}:
params.reject!{|k,_|[‘controller’, ‘action’].include? k}
params.permit(:name)
SomeModel.new(params) #Exception, world is OK
SomeModel.new(params.symbolize_keys) #No Exception, secret overwritten
symbolize_keys returns an object of type Hash, that no longer has the
‘permitted?’ function, so strong parameter protection is effectively disabled.
Both of these patterns are found quite a bit in our codebases - often in a not
such simple form though.
In some sense the problem is, that there is non-framework code on the
codepath between where the whitelist is defined and where it is used, and it is
easy to accidentally lose the whitelist which disables mass assignment
protection. This problem did not exist with protected attributes, since there
was no codepath.
Using ‘attr_accessible :name’ on the model would have prevented these problems,
however I am under the impression that strong_parameters is to replace this old
way.
Am I the only one worried about this (I have not found any discussion online) or
is this a known problem? If it is known, what is the proposed solution?
I understand that developers need to take care when handling user input, and
such should not call e.g. symbolize_keys. But the same is true for mass
assignment vulnerabilities as a whole - in a world where programmers do not make
mistakes mass assignment errors do not exist.
A possible solution could be to not attach this capability onto the parameters
Hash but store it separately and then retrieve it at mass assignment time - so
it is not the developers responsibility to babysit it. In an alternative
implementation ‘permit/permit!’ could be used to declare a list of mass
assignable parameters for a given action in a controller (optionally on a per
model basis, default empty list) and store it on the request or as a thread
local variable. The mass_assignment part on the model would then read this
variable and decide if the mass assignment should be allowed or not. This way
the capability/whitelist can not be accidentally lost by transforming the
parameters hash anymore.
best,
Johannes
[1] https://github.com/rails/strong_parameters/blob/master/lib/action_controller/parameters.rb