-
Notifications
You must be signed in to change notification settings - Fork 21
Description
It's great to see this gem updated to deal with Rails 4 and 5, but the integration with the (then-)new strong parameters machinery seems to have a few rough spots. I've got code that attempts to deal with these issues on a branch of my clone, but figured I'd discuss the issues first before making it a formal PR.
The most serious is that even when a controller defines an explicit whitelist for parameters, as "#{resource_name}_params", it can be bypassed in certain circumstances. There's actually an illustration of the problem in the test suite. The CommentsController
in the sample app.rb
defines comment_params
as follows:
def comment_params
params.fetch(:comment, {}).permit(:user_id)
end
The obvious intention here is that only :user_id
be settable through mass assignment. But if you look at comments_controller_spec.rb
, there's
def do_post
post :create, params: { :comment => {:name => 'Comment'}, :forum_id => '3', :post_id => '2' }
end
it "should build a new comment" do
expect(@post_comments).to receive(:build).with({'name' => 'Comment'}).and_return(@comment)
do_post
end
If the comment_params
whitelist were being applied, you'd expect this to fail, as the whitelist has only user_id
and not name
as permitted parameters. In fact, it passes. The reason for this turns out to be this code in resource_methods.rb
:
def new_resource(attributes = nil, &block)
if attributes.blank? && respond_to?(:params) && params.is_a?(ActionController::Parameters)
resource_form_name = ActiveModel::Naming.singular(resource_class)
attributes = params[resource_form_name] || params[resource_name] || {}
end
resource_service.new attributes, &block
end
What happens is that the create
in actions.rb
invokes new_resource(resource_params)
, which invokes comment_params
. So far, so good. However, comment_params
sees no parameters on its whitelist, and so returns an empty ActionController::Parameters
. That answers true to blank?
, which causes the code quoted above, in new_resource
, to go digging around in params
directly, bypassing the whitelist. If this code were invoking resource_params
directly instead, to get a default set of parameters, this anomaly wouldn't occur.
The behavior of resource_params
itself has a couple of other oddities. First off, if a controller defines #{resource_name}_params
, that gets invoked -- but if the controller tries to override resource_params
by itself (e.g.,
def resource_params
params.fetch(:comment, {}).permit(:user_id)
end
it doesn't work. The problem is that the controller's own definition of resource_params
is shadowed by the one in resource_methods.rb
, which never tries to invoke super
. This can be dealt with by adding an if defined?(super)
to invoke the controller's own definition if it exists.
The last issue I've found here is that if no explicit _params
method can be found, the default resource_params
in resource_methods.rb
assembles a whitelist for itself out of known column names. That mirrors the permissive default behavior of Rails 3 and earlier, in which the default policy (unless you asked for something else) was to permit mass-attribute setting in create
or update
to alter any attribute. However, the Rails team made a deliberate design choice to move away from that policy when they did strong parameters, because it had proven to be an unsafe default. It's still possible to get a permissive policy if you ask for one, e.g.:
def comment_params
params.fetch(:comment, {}).permit!
end
but in stock Rails, you need to explicitly ask for it; you don't get it out of the box.
(An example of the problems that prompted this change -- in fact, the cause for it -- was on Github itself. A whiteish-hatted security researcher who thought Rails needed stronger checks updated the user_id
of one of his own ssh keys to transfer it to a committer to the Rails project itself, and used it to make a commit onto Rails master. His commit was benign -- it just added a text file -- but it was enough to illustrate the danger of allowing stray attributes, such as user_id
to be set through forms that weren't expecting them.)
To harmonize with this design intent, my proposed changes remove the default whitelist. The proposal is that it's still possible to get a permissive policy for some or all of your controllers, by an explicit
def resource_params
params.fetch(resource_name, {}).permit!
end
in a particular controller, a base class for a set of them (e.g., AdminController
, if all your admins have console access too, and access controls are pointless), or for quickie throwaways, in ApplicationController
itself, but, as in base Rails, you should have to explicitly ask for it.
As noted above, I do have code to deal with all these issues (somewhat intermingled). Most of the changes are to specs, to make sure that they try to set only parameters that are on whitelists. (For the most part, the whitelists already were there in app.rb
, but because of the new_resource
issue, they were being ignored in a lot of cases.) But I thought I'd raise them for discussion before opening a formal PR...