January 3

Written by Team

Rails parameter wrapping

Rails has a lot of helpful stuff under the hood, particularly for routing, processing, and transforming incoming HTTP requests. For example, thanks to the params hash in Rails controllers we need not worry about:

The nesting structure of incoming parameters generally depends on the type of requester and the content type of the request body. Requests generated by browser form submissions will nearly always have parameters nested in an outer key. So, the params hash will look like this:

# POST /people

{

  person: {

    name: “Slartibartfast Jones”,

    email: “slarty@example.com”

  }

}

Alternately, requests sent in JSON format by a JavaScript or mobile client will, generally, not include the outer key:

# POST /people

{

  name: “Slartibartfast Jones”,

  email: “slarty@example.com”

}

Old-style Rails controller conventions dictate a top-level key (e.g. @person = Person.create(params[:person])). New-style strong parameters expect a top-level key (e.g. params.require(:person).permit(:name, :email)). Most importantly, when writing our controllers we shouldn’t have to care if the request came from an HTML form, a JavaScript client, or the HTTP-compatible death ray from an alien spaceship. And, for the most part, we don’t have to. In the case with no outer key, Rails creates an outer key and wraps the incoming parameters in it via the innovatively named wrap_parameters feature. Except when it doesn’t.

The problem

In order to work automatically, the wrap_parameters functionality makes a couple assumptions about your controller:

Much of the time these assumptions hold, and everything works. However, as an application becomes more complex it may begin to violate these assumptions, at which point you’ll need to get a little more actively involved.

Key name mismatch

Our PeopleController behaves much as you’d expect, but perhaps you also want to generate exports files containing all available people. You could create the PeopleExportsController, but if you also create other types of exports, and would like all export-related controllers to group together, you may create the Exports::PeopleController. The wrap_parameters functionality does try to consider namespaces, but the difference in naming conventions between models and controllers (singular vs. plural) makes success unlikely. So, for the Exports::PeopleController, it will wrap incoming parameters in the person key by default. Oops.

We can solve this by explicitly telling your controller what parameter wrapping key to use:

class Exports::PeopleController < ApplicationController

  wrap_parameters :export

 

end

Easy peasy.

Model mismatch

Our Exports::PeopleController now wraps incoming parameters in the correct key, but it still has to decide which incoming parameters to wrap. It still thinks the Person model is the model it cares about, so it will call Person.attribute_names to determine which parameters to wrap. Chances are the Export::People class and the Person class don’t have the same parameters. Oops.

We can solve this by adding some more information to what we tell the controller:

class Exports::PeopleController < ApplicationController

  wrap_parameters Export::People, name: :export

 

end

Still fairly simple, but starting to become a little more onerous. Note we still need to specify the wrapper key name.

Attributes mismatch

Finally, perhaps our export class has an optional attribute sync that allows the requester to specify that it should execute immediately, rather than asynchronously; it doesn’t persist this attribute, but simply uses it to determine its behavior on creation. Since the attribute_names method returns only the names of columns associated to our class’s database table, it will not include the sync attribute, and the wrapped parameters will not include sync. Oops.

We could override the attribute_names method on our Export::People class, but once we start leaking controller concerns into our models suddenly we’re on the road back to attr_protected and attr_accessible. Dark days, indeed.

Instead, we can use another options on the wrap_paramters call:

class Exports::PeopleController < ApplicationController

  wrap_parameters Export::People, name: :export, include: %i[format pattern sync]

 

  def export_params

    params.require(:export).permit(:format, :sync, patterns: [])

  end

end

Note that we have to pass all parameters we want wrapped to the include option; if you provide the include option your controller will wrap only the parameters you specify. Note also that we’re starting to duplicate (almost) a lot of the arguments for strong parameters.

At this point we have to start wondering if this is all worth it.

We want a correctly structured params hash, but why does the parameters wrapper have to jump through so many hoops to determine exactly which parameters to wrap? In fact, why does it care at all? Restricting the incoming parameters to an accepted set is the responsibility of strong parameters, not the parameters wrapper. Let’s just wrap all incoming parameters and skip all this complexity.

Unfortunately, there’s no option to turn off the default parameters inclusion/exclusion behavior, but it’s a fairly simple patch (seen here). This small patch actually eliminates a nasty chunk of code (seen here and here, including some thread concurrency locking. Good riddance!

Security considerations

But wait! (shouts the astute, and security conscious, reader); doesn’t allowing the controller to wrap arbitrary parameters make the server more vulnerable to attacks? You can never go wrong considering security, so let’s look for potential holes.

Assuming we’ve used strong parameters properly our controller shouldn’t consider an unacceptable incoming parameter, whether it’s wrapped or not. An attacker can send in { admin: true }, but even if we wrap that into { user: { admin: true } } the strong parameters mechanism will strip it out as necessary.

We do potentially allow an attacker to attempt to overwhelm the system, DoS-style, by sending an arbitrarily huge number of parameters with arbitrarily large amounts of data. The parameter wrapper will make a new hash with all wrapped parameters, potentially using a large amount of memory and/or CPU. Fortunately, the wrapping process uses Hash#slice to create the new hash, so while the new hash will have to allocate memory for its keys, the values point at the same values as the unwrapped hash. Considering Rails already allocates memory for all incoming parameters prior to wrapping, an attacker could attempt to overwhelm the server with or without wrapped parameters. Wrapping all parameters can increase the number of objects allocated for parameters by a factor of two, but considering the number of parameters required for this type of attack this hardly expands the attack surface.

Testing

But wait! (again with the shouting); how do we test that we’ve wrapped parameters properly? An excellent question, and, unfortunately, a testing aspect that Rails controller tests don’t support. We can test parameter wrapping indirectly in system/integration/request tests, but those types of tests seem like overkill when we just want to specify that a controller wraps parameters properly. Perhaps that will be our next small addition here.

 

«