For a while, I have felt that the following is the correct way to improve the mass assignment problem without increasing the burden on new users. Now that the problem with the Rails default has been brought up again, it's a good time to revisit it.
When creating a form with form_for
, include a signed token including all of the fields that were created at form creation time. Only these fields are allowed.
To allow new known fields to be added via JS, we could add:
<%= f.allowed_fields "foo", "bar", "baz" %>
The first strategy will not full satisfy apps that support a lot of HTTP requests that do not come from forms generated by Rails.
Because accessible fields is usually a function of authorization, and is not global, we should move allowed fields into the controller. The basic idea is:
class PostsController < ApplicationController
# attributes can be marked accessible for the entire controller
attr_accessible :foo, :bar
def create
# mass assignment can also be done on an instance basis
# this can be used to override the class defaults
attr_accessible(true) if user.admin?
...
end
end
I would imagine that Rails authorization frameworks like CanCan could add sugar to make this even easier in common cases.
The core problem with Rails mass assignment is that attribute protection is an authorization concern. By moving it to the controller, we can have smart defaults (like signed fields in form_for
) and in more advanced cases, make it easier to decide what fields are allowed on a per-user basis.
By moving it into the correct place, we will probably find other nice abstractions that we can use over time to make nice defaults for users without compromising security.
Just to add my 2 cents here.. I don't mind the idea of moving mass-assignment protection to the controllers (or even to a Django-style Form object). I don't think it's necessary, but I don't mind, it won't be much of a learning curve (and only a very minor inconvenience) if that happens.
I don't like the idea of defining the allowed attributes as a hidden form field, signed or not. Sure, it sounds like a great idea of you're using server-rendered ERB, Haml or Slim templates with the form_for helper.. but what happens if you're doing your front-end in Backbone or Ember and using Handlebars templates? How are we passing that hidden form field to the front-end templates that are rendered by Javascript in the browser and not by Ruby on the server? Are we just ignoring them in that case? If so, it defeats the purpose of having them in the first place. Considering that more and more apps are moving to a frontend Javascript framework, Rails really needs to continue to support both use-cases.. everything rendered on the server, or everything passed back and forth as JSON and rendered in the browser (and even the possible third use-case, mixing those two strategies).