Description
tl;dr: it's too easy to write Grape endpoints that are not protected against Mass Assignment abuse.
I've created an example Rails application called PretendGravity
, an homage to the main API server at Artsy called simply Gravity
. I added a model called Artwork
and then implemented CRUD endpoints without using Grape - just plain old Rails controllers. Then I installed Grape on the project and implemented those same CRUD operations using a Grape endpoint. I wrote request specs to verify that the two sets of endpoints behave the same way.
Then I created a PR that implements something new - adds a new field to the Artwork
model that should not be allowed to be set via the API. I used this PR to demonstrate that it's too easy for a Grape user to write endpoint code that is not protected from Mass Assignment abuse. The last commit on that PR adds a failing test that shows the abuse in action. That same type of test was written on the Rails side and passes because over there we have protection via Strong Params from Rails.
Then I wrote a series of mitigations that might be attempted.
Mitigation One: use except
The first mitigation I attempted was using the except
method that removes a key from a hash. This passes the failing test but I'm not a fan because:
- every new field that's added would need to be removed
- an engineer would have to know that they have to use this method to keep their params safe
Mitigation Two: use declared
The second mitigation uses a helper from Grape called declared
. There were two steps here:
- specify the params for the create and update endpoints
- wrap the
params
with thedeclared
method and specifyinclude_missing
asfalse
This approach filters out params that are unknown and passes our failing tests. Even better it's future proof in this endpoint so that should a field be added in the future then it does not have to be manually added to the list of exceptions like in the first mitigation.
Still, as in the first mitigation, an engineer would have to know that they should use declared
and future endpoints have no build-in protections. This one is better than the last mitigation but imho is still insufficient.
Mitigation Three: use Strong Params
The third mitigation brings patterns from Rails into Grape by using Strong Params and an artwork_params
helper to pass the failing specs.
Failing tests are back to passing but we still have only addressed this particular endpoint and haven't done enough to protect our future selves from making similar mistakes.
Mitigation Four: set Strong Params as param_builder
The fourth mitigation takes the idea of using Strong Params a step further. Using the param_builder
configuration option of Grape I created a shim module that turns all params
objects into ActionController::Parameters
objects. I still had to add a helper for artwork_params
and call permit
with the allowed params as both the third mitigation and Rails controller must.
What seems better here is that our current endpoint and all future endpoints now benefit from the built-in protection offered by Strong Params. It's not just the filtering of params, it's also that if used incorrectly an exception is raised:
params = ActionController::Parameters.new({})
artwork = Artwork.create(params)
# => ActiveModel::ForbiddenAttributesError raised!
Proposal for Adding Strong Params to Grape
So this was all in service of this: I propose we add Strong Params to Grape. There are already 3 options so there are patterns we could follow. Docs could be updated and should probably have a section about Mass Assignment and the stance of the Grape project. I think this would be a great first step in improving protections for Mass Assignment abuse.
I would take it further though. I would propose that eventually Strong Params should be the default and then even remove the other options altogether. Maybe it happens over the course of a few major releases to ease the upgrade pain but I believe it should happen. Rails had to go through this pain but now lots of apps are much more secure than they were before and I think Grape should have this reckoning too.
Am I Missing Something?
I've looked at this every which way I could think of but maybe I'm missing something - are there other mitigations that I'm not thinking of? Is there a better way to address Mass Assignment abuse in Grape? How would you tackle this one?
And it's possible that adding Strong Params to Grape has already been discussed - let me know if there's something I can catch up on! Ultimately what I'm after is better Mass Assignment protection in Grape. I believe Strong Params is the right way to address this but def open to hearing more.