Mass assignment security shouldn't happen in the model

Mass assignment security is a feature in Active Model (and therefore Active Record) which allows you to be selective about what input you will accept when ‘mass assigning’ a hash of attributes.

Typically, you will have a form that submits to a controller update action, which simply assigns all the form parameters to the model:

@user.attributes = params[:user]

Any keys in params[:user] which are not allowed (e.g. an ‘admin’ flag) will be silently discarded.

Problems

This is great, of course, because nobody wants to get hacked. However if you have been writing Rails applications for long enough you will come up against some problems:

  1. Creating records in tests. Often when testing models, you will want to create a model with a given set of attributes, and then interact with it in the actual test. The quickest way to do this might be

    user = User.new(:admin => true)

    but if you have the admin attribute protected, it won’t work. Worse, you won’t know it hasn’t worked: the attribute is just silently ignored.

    There are some ways around this:

    • The most popular ‘object factory’ plugins will ignore mass assignment security when creating an object from a factory.
    • Recently a feature has been added in edge Rails to raise an exception in the development and test environments when mass assignment parameters are filtered. This will not be available until Rails 3.2, and you will need to set config.active_record.mass_assignment_sanitizer = :strict in your environment config to enable this.

    (In general, this problem is better described as ‘interacting with records somewhere other than a controller’.)

  2. Different rules for different people. Often there are different ‘roles’ for users in an application. For example, an ordinary user should be able to change her password, but not promote herself to an admin. However, an admin should be able to promote another user to be an admin.

    This has historically caused problems with mass assignment security, so in Rails 3.1 (not yet released), there is a mechanism for specifying ‘mass assignment roles’.

    Essentially, you can specify different lists of allowed attributes, and give them a name. Then, when an admin user is editing a user, you can use their ‘list’ via the :as option:

    @user.assign_attributes(params[:user], :as => :admin)
  3. Active Record has to work around mass assignment security internally. This isn’t so much an end user problem, but in the past few months I have fixed some very nuanced bugs within Active Record. These bugs were either caused by mass assignment security kicking in where we didn’t want it to, or by subtle mistakes in the logic when we tried to work around the mass assignment security. (If you’re interested, see the discussion in these bugs and these commits.)

Put it in the controller

Mass assignment security is a feature that is aimed at dealing with form input. By placing it in the model, we end up making the implicit assumption that all ‘normal’ interaction with the model happens via a HTTP request. This assumption is incorrect and causes problems.

I believe that the filtering should happen in the controller, which is the area of the application which is supposed to deal with HTTP input. Here’s a rough sketch of how it might work:

class ActionController::Base
  include ParamSecurity

  before_filter :filter_params

  def filter_params
    self.params = param_filters.inject(original_params) do |mem, filter|
      filter.process(mem)
    end
  end

  # Define filter(s) which operating on incoming params. Can be overridden to
  # extend/customise the filter list.
  def param_filters
    [param_security_filter]
  end
end

module ActionController::ParamSecurity
  extend ActiveSupport::Concern

  module ClassMethods
    included do
      class_attribute :param_security_rules
    end

    # Some sort of dinky DSL so you can do e.g.
    #
    #   param_protected :user => [:role, :email]
    #
    # or
    #
    #   param_accessible :user => [:name, :password], :only => :create
  end

  # Can be overridden if necessary
  def param_security_filter
    ParamSecurityFilter.new(self.class.param_security_rules, action_name)
  end
end

Why this is better

  • We can do away with having ‘roles’ for mass assignment security; you can simply define different filters in your UserController and your Admin::UserController.
  • When outside the controller, we can use models normally, without having to worry about security features that don’t apply to the current situation.
  • It’s totally customisable on a per-controller or even per-action basis.

What do you think?

I am not the first to make these observations. Merb had a plugin to do this, and a similar plugin has been written for Rails.

But mass assignment security is one of the first things a new Rails user learns, and it’s a feature that is present in almost every single Rails application. So personally I would love to see this changed in Rails core itself.

Of course, it’s not that simple because we have to think about backwards compatibility, and it could be quite annoying for Rails users to have to move the filtering rules out of their model and into the controller.

So I am not sure whether this is really worth changing, but I would like to hear your thoughts!

Comments

I'd love to hear from you here instead of on corporate social media platforms! You can also contact me privately.

Pavel Forkert's avatar

Pavel Forkert

I think the main problem is that Rails is Model-centric and a lot of functionality is included into models itself - mass assignment is only one of the parts, which can be implemented outside of models - filtering/sanitizing parameters, additional mapping layer between forms in views and model attributes, different validations for different roles, etc. Many big apps would be happy if Rails provides a way to clean up their models a bit more. :)

But I think - trying to solve all those problems will lead to more difficult APIs and many newcomers or people, that are building small/medium apps will suffer :)

Personally I am more or less happy with current mass assignment APIs. :)

dcrec1's avatar

dcrec1

I think it depends, some things make sense to be protected at the model, other at the controller. I don't think there is a rule for this, would be cool if Rails supported both ways.

Giles's avatar

Giles

I think as always with Rails you want a balance between reasonable defaults and ungodly power for those who wish to cause mischief. However, I also think the way to go about this is to throw a few different plugins together and see which work best. Reason I say that is I can see a variety of possible APIs working well, and whichever API we end up with, transitioning people from the flawed "it lives in the model" approach will take some time and effort. Best way to deal with that is to throw a bunch of options out there, build some real live apps with them, and see which options have which strengths. TATFT applies to features and design too. :-)

jcasimir's avatar

jcasimir

I respect the idea, but have to disagree. I see attribute security as a data/business-logic concern which means it has to go in the model layer. If you want to factor it out into a module that gets included into models, that sounds interesting. But handling it at the controller level feels inappropriate.

I think you'd be better off creating a "factory-like" class method down in the model and using that instead of ActiveRecord's class methods from the controller.

ultrasaurus's avatar

ultrasaurus

I used to do this in controller logic because, in principle, I felt that external APIs should be locked down in the place that you define the API and that the model layer is an internal API where security shouldn't be a concern. However, in practice, I only use mass assignment from controllers and it is more concise to define attr_accessible in the model, so that's what I do now. I suppose I just think of the mass assignment API now as a public API.

Joost Baaij's avatar

Joost Baaij

I also feel that, in principle, the role and mass-assignment security features should move to the controller. Since parameters submitted by admins are equally valid to parameters submitted by users, the model should not have to care about the distinction. Since bots sets are valid, the concern should be lifted out of the model and into the controller.

However for the 3.x series of Rails I think we're stuck with the current practice. As you said, more or less every Rails developer will have seen this problem and dealt with it in some way. For me, it's an acceptable tradeoff and just one of those things you need to learn in order to become proficient doing Rails.

Your proposed solution will work fine, but still entails hacking at hashes and, by extension, the manipulation of strings. I would rather see an effort to move away from dumb string manipulation in the params/controller and implement something akin to Arel which we did for the model. In other words: a very much souped up controller that we can also use as presenter/conductor. In every Rails app I am doing, I have some need for these patterns and I end up implementing them myself. For Rails 4 I think it would be great if the C from MVC could rise above its current self.

Moving these mass-assignment features into that layer would be a very natural thing to do.

Rasmus Rønn Nielsen's avatar

Rasmus Rønn Nielsen

When I heard Rails was introducing a mass assignment security feature I was like "Yeah, finally"!!

When I found out it happened in the model I was like "Wait.. what??". I would have expected it to be happening in the controller.

Your argument about how the current implementation "assumes" that HTTP is the normal way of interacting with a model is key.

Failing silently works great when setting attributes via http params. But in almost any other cases, it's not (tests, rake tasks, etc).

Okay, we may be setting attributes via http (params hash) most of the time, but does that mean it is okay to make this security feature annoying when we're not? I don't think so.

Also, by sending :as => :admin to the user, we're slightly coupling ActiveModel with the entire app's authorization logic - that smells if you ask me. The model shouldn't know anything about the context in which it is used.

J.-Baptiste Escoyez's avatar

J.-Baptiste Escoyez

Update: Disqus did not show the whole thread when I first wrote the comment. @Joost I wrote about your presenter/conductor idea hereunder. @Rasmus I so much agree on "The model shouldn't know anything about the context in which it is used."
Update 2: I had not saw that: https://github.com/jonleigh... :)

@Jon, @Pavel: I really agree with you!

I started to use what I call a Conductor (not totally sure it match the conductor pattern) in my applications.

Basically, it is a class wrapping the object which will filter parameters and format them for assignment. So that, only the business logic is written in models.

E.g.: In my UsersController, you can find:
> def update
> @user = UserConductor.find(params[:id]) # Which create a UserConductor object wrapping the User instance
> @user.attributes = params[:user] # attributes is overridden in UserConductor in order to filter attributes (e.g. "100$" > 10000 (when dealing with amounts)) and assign them to the model
> @user.save # other ActiveRecord methods are just transmitted to the wrapped model
> end

I think mass-assignment could have its place their too since, IMO, the role of the controller is not to filter params, just to forward them to the right actor (Model, View, Mailer...)

Jon Leighton's avatar

Jon Leighton

This is a very good point and I think a conductor is the cleanest solution to this (as well as for dealing with stuff like nested attributes). I actually tried to write something along these lines several years ago: https://github.com/jonleigh... (it's unfinished, abandoned and probably doesn't work with rails 3)

On the one hand I would love to see Rails embrace stuff like conductors and presenters/decorators (see also: https://github.com/jcasimir... ) out of the box. But on the other hand to do so would be to steepen the learning curve for new users. It's a tricky balance...

J.-Baptiste Escoyez's avatar

J.-Baptiste Escoyez

Could we imagine a gem bundle called "advanced_rails" which will provide some advanced architecture to the basic Rails MVC ? These gems could integrate nicely an be integrated to Rails if needed/wished.

Alexey Muranov's avatar

Alexey Muranov

I agree. It always looked strange to me:
1. before rails 3.1, it looked strange that the same rules of writing data apply to all cases and all users,
2. in rails 3.1, it was solved with roles, but now it looks strange that a model knows about roles of _people_ who use it; it is like adding roles for accessing public methods of a class.

Alexey Muranov's avatar

Alexey Muranov

I disagree with the objection: the security here means protecting from malicious or non-careful users, not from the developer or other parts of the application. So in my opinion its place is in the controller layer. (Even more specifically: usually it means simply filtering the params hash.) What is exactly "attribute security"? Attributes are to be read and written by the application.

Alexey Muranov's avatar

Alexey Muranov

Another argument in favour of the change: it would be easier to follow if the security of each web request was handles in a single place (to know what code is responsible). Currently the controller has the choices: (a) blindly delegate filtering to the model, (b) filter itself before delegating to the model (why not?), (c) bypass model-level "security" by not using mass assignment.

Charles Hoffman's avatar

Charles Hoffman

I am totally with you on this. I feel similarly about accepts_nested_attributes_for.

Alexey Muranov's avatar

Alexey Muranov

I would like to reply as a "new user". I've been reading now about conductors and presenters. I think if their use will be optional, this will not steepen the curve. I've spent quite some time trying to understand why params are filtered by the model and why it is ok to rely on this filtering (i still no not understand very well). It would be faster for me to learn how to filter them with a `before_filter` (until i would learn how to use a conductor), and i wouldn't be so confused.

dpickett's avatar

dpickett

I totally agree that it's not a controller behavior or responsibility. There's an intermediary business object that's missing I think. Something like a conductor/decorator pattern seems appropriate here. Some kind of AttributeFilter abstraction that can be applied directly to model instances, instantiated in the controller.

Nicolas Sanguinetti's avatar

Nicolas Sanguinetti

Have you seen how Django does this? It defines objects for the "Forms" that handle validations. There's a ruby implementation of this for ruby called Bureaucrat: http://github.com/tizoc/bur...

Eleo's avatar

Eleo

The pattern presented here seems viable/logical. I just don't see it as an improvement -- just a different approach.

I can agree to some extent with some of the problems presented, but I don't feel that using the controller to filter attributes is a solution superior to preventing mass assignment within the model.

I see this as akin to using the controller for validations. Validations in the model introduce some of the same problems as handling mass assignment from the model. For example, in tests, sometimes I have to bypass validations in the same way I might have to force mass assignment. Validations also vary based on role -- e.g. an admin might have fewer restrictions on making posts than a regular user. In my eyes, a model accepts all the incoming data and then sorts through it as necessary. It's object-oriented and highly testable.

>Mass assignment security is a feature that is aimed at dealing with form input. By placing it in the model, we end up making the implicit assumption that all ‘normal’ interaction with the model happens via a HTTP request.
I don't see how this assumption is invalid, or bad. If you're preventing mass assignment from the model, then it's because your application deals with submitted data. There might be further interactions, such as within tests, irb, background jobs, I don't know. I haven't found it to be too problematic outside of these contexts. These are my problems, not the user's. If your model doesn't deal with submitted data, it probably doesn't deal with mass assignment or doesn't need to. I don't see what the problem is here. Maybe I'm missing something.

You bring up some interesting points for sure. What I'm getting from this is that perhaps we are following MVC too stringently. For example, going back to the validations in the model -- it always bugged me, typing up validation error messages in my models, because it seemed like a view concern what that message is. The paradigm should not be immutable and maybe we need to think beyond "is this a model, view, or controller?" and insert layers as necessary without trying to categorize them necessarily as one of those three things.

joelparkerhenderson's avatar

joelparkerhenderson

> There's an intermediary business object that's missing I think.
> Something like a conductor/decorator pattern

You're looking for DCI: Data Context Interaction. It's perfect for what you're suggesting.

Alexey Muranov's avatar

Alexey Muranov

@Jon: what do you think about simply isolating non-database related functionality of a model using inheritance:

```ruby
class Employee < ActiveRecord::Base
end

class Accessors::Employee < Employee
attr_accessible :name
attr_accessible :salary, :as => :boss

def self.controller_class
@controller_class || StaffController
end

def self.controller_path
controller_class.controller_path
end
end
```

This is another layer between Model and Controller (or Model and Conductor), and can be already used in rails.
(See my comment in https://github.com/rails/ra... )

Doug Puchalski's avatar

Doug Puchalski

This is something that has troubled me about Rails since I started working with it. The idea that forms on a web page are directly tied to the persistence layer seems odd. Why should the view show the names of my DB columns? And why should the model, which has no idea of the context, decide what error messages to display?

Persistence, Presentation, and Conduction are quite like independent functions. For simplicity and learning, mapping them directly to MVC can make sense. Using these patterns is just the next level, a good way to keep code clear, controllers thin, ease refactoring, and be fail-safe.

I am leaning toward the following:

Model: (persistence) I choose the schema and methods that map the required data model into whatever storage mechanism I want, making it easier to refactor. No security is handled, only low-level validations that affect data integrity of each instance. Validation messages can be the defaults.

Conductor(s): (accessibility) One or more for each model or set of models. Uses ActiveModel (ActiveAttr?) to define attributes and validations that are appropriate to the context. Operations that result in a valid conductor instance are delegated to the model.

Controller(s): (authorization) Based on authorization or mode, a controller would choose and act on the appropriate conductor. A lot of filters and other tangles are gone, and more cleanly implemented.

Presenter(s): (presentation) Whitelist delegation to the model, removing need for model helpers entirely. Again here the notion that a user vs. admin view of a resource may be entirely different.

View(s): (formatting) now safe from accessibility concerns, the view only has access to it's whitelisted presenter. Views become more like templates, less like code. Accidentally showing the wrong data is much harder, and less diligence is required. I'm sure I'm not the only one who has accidentally shown something in the view either by editing the wrong file or simply with a typo.

Standard MVC works fine in the beginning or with some models, and splitting things out might be done on a case-by-case basis as complexity increases.

Add your comment