How to convert to `params.expect` in Rails 8.0
After updating RubyGems.org to use the new params.expect
feature
in Rails 8, I thought it might be helpful to go over a few of the challenges I ran into.
Why Should I Convert to params.expect
?
The new expect
method for filtering params protects against user param tampering that can cause hard to rescue errors.
As a quick review of the feature, when we have code like this:
We’re vulnerable to users calling our action like this:
By using the new params.expect
we can prevent this problem (and another I’ll discuss below) all while cleaning up our params handling.
Remember that with valid params, the values at the expected key(s) will be returned, just like with require
.
The Easy Part
The conversion follows a consistent pattern.
Take the symbol (:user
) that you put into require
and instead call expect
with a hash, using this symbol as the key.
Move the args you had in permit
(:name, :handle
) into an array value for your key.
In general, the args for expect
should match the structure of the params you expect in your controller.
You can search and replace for the most basic changes. Use the following find and replace regex as a starting point. However, there will be some cases you can’t fix this easily.
If you don’t have good test coverage, be careful changing any permit
that has an array in the args (like permit(key: [:other, :keys])
).
Find `params.require\(:([^\)]+)\).permit\(([^\)]+)\)`
Replace `params.expect($1: [$2])`
I suggest converting the simple ones and leaving the complex ones for later.
After making the easiest changes, search for permit
and find anything with multi-line or more complicated code.
Once you’ve converted everything, run your tests. There might be some things that broke The most common reason for test failures will probably be the new params array syntax.
Explicit Arrays
Because params.expect
requires explicitly declaring expected Arrays, you’ll need to fix those manually where they occur.
This is because params.permit
did not distinguish between [:key]
meaning an array of hashes, or a single hash.
This could lead to some input problems in users send incorrect params.
Hopefully your tests will fail with something like this:
Expected response to be a <2XX: success>, but was a <400: Bad Request>
Here’s a more complex example I ran into today:
The problem is that statements_attributes
should be an array of hashes.
To require that key to be an array, params.expect
needs us to wrap the array in second array.
I left a lot of the extra detail here so you can see what does and does not change.
Note how some of the arrays in the example don’t change and some do.
The first one that I commented as “doesn’t change” is api_key_permissions
which expects a Hash value with 3 keys, 2 of which are arrays. The empty arrays will allow any scalar values (numbers, strings, booleans), but not nested hashes or arrays.
The fix here is to add a double array for key statements_attributes
and conditions_attributes
.
This requires the value to be an array of hashes like the following (simplified, nesting reduced):
A Hash of Hashes with IDs for Keys
The next one stumped me for a bit before I tested it in the Rails console. The controller seemed very simple and I thought nothing of it during my search and replace.
However, our tests failed with the telltale params.expect
failure:
Expected response to be a <3XX: redirect>, but was a <400: Bad Request>
I looked at the form and it sends something like the following, which I put it into the Rails console (bin/rails c
) to test:
I thought this would be tricky, but Rails StrongParameters has rules that treat this like an array. The fix is just the same as the others: add a second set of array square brackets.
Usually the changes should be this simple. Find single arrays that need to become explicit double arrays.
Handling _json
Params
If your application uses the Rails _json
key, the conversion here always requires an explicit array.
Rails adds this key to params when JSON is posted with an Array root instead of a Hash.
This new format ensures that you’re always receiving an array. While this doesn’t directly fix vulnerable code, it should make you think more clearly about what params you’re expecting. This in turn could help mitigate a potential vulnerability enabled by inexplicit verification of input params.
Go Forth and Expect
Mostly you can rest assured that if your original permit
worked, then Rails already knows how to parse the input.
The changes made to expect
don’t change how rails parses permitted params.
The new behavior distinguishes between expected arrays and expected hashes, requiring you to choose which one your controller needs.
You can see all the changes I made in the GitHub PR for RubyGems.org if you’re looking for real examples.
Anything I Didn’t Cover?
I can imagine a few more situations that might be tricky, but I’m assuming that if you encounter any of these, you’re already doing some tricks with permitting in your existing controller. Make sure your tests cover the situation and then see if you can use expect
.
If you encounter more problems, please let me know on whatever social you can find me on. (maybe check the homepage?)