Skip to content

'resource_name'_params whitelists can be bypassed (and other strong params glitches) #14

@rst

Description

@rst

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...

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions