Benjamin Wood - April 10, 2020

Rails announced strong_parameters as a replacement for protected_attributes nearly eight years ago. In 2020 many Rails apps have not completed the migration. Others have made the migration, but are worse off than before. Was strong_parameters a bad idea? I don't think so. But like many things, it depends on how you use it.

Strong Opinions

Instances of ActionController:Parameters leak outside of the controller

This is undoubtedly the most common mistake I've seen. The reason that strong_parameters is superior to protected_attributes is that it narrows the concern of mass-assignment protection to the controller. With protected_attributes , the entire app had to consider which attributes could mass-assigned, and which could not. This led to anti-patterns like the use of without_protection which allowed developers to skip mass-assignment protection under certain circumstances.

The benefit of strong_parameters is lost when ActionController::Parameters are passed outside of a controller and suddenly you have to be concerned about mass-assignment protection everywhere, again.

class PaymentProcessor def initialize ( params ) @params = params end def process Payment . create ( payment_params ) end private def payment_params @params . require ( :payment ) . permit ( :some , :payment , :attrs ) end end class PaymentsController < ApplicationController def create payment = PaymentProcessor . new ( params ) if payment . process . . . else . . . end end end

Strong Opinion: Parameters should be permitted at the controller level. If you pass some params on to another object (like a service class), first permit the params that are allowed for mass-assignment, then call to_h on it to convert the object to a good ol' ActiveSupport::HashWithIndifferentAccess .

class PaymentProcessor def initialize ( payment_params ) @payment_params = payment_params end def process Payment . create ( @payment_params ) end end class PaymentsController < ApplicationController def create payment = PaymentProcessor . new ( payment_params . to_h ) if payment . process . . . else . . . end end private def payment_params params . require ( :payment ) . permit ( :some , :payment , :attrs ) end end

Use of strong parameters where mass assignment is not being performed

The Rails community is pretty split on this one. Should you always permit parameters? Or only permit parameters when mass-assignment is being performed? Here's an example:

class UsersController < ApplicationController def special_update_action user = User . find ( user_params [ :id ] ) user . special_attribute = user_params [ :special_attribute ] if user . save . . . else . . . end end private def user_params params . require ( :user ) . permit ( :id , :special_attribute , :some , :user , :attrs , :including ) end end

Strong Opinion: Don't complicate your controllers by permitting params when mass assignment is not being performed.

class UsersController < ApplicationController def special_update_action user = User . find ( params [ :id ] ) user . special_attribute = params [ :special_attribute ] if user . save . . . else . . . end end end

Order of operations when mutating a params object

Remember, strong_parameters is meant to sanitize externally-provided parameters. If you add a key to the params object, then call permit , you'll end up having to permit the parameter you just added.

class CreditCardsController < ApplicationController def update credit_card = CreditCard . find ( params [ :id ] ) params [ :top_secret_token ] = TopSecretToken . new ( args ) if credit_card . update ( credit_card_params ) . . . else . . . end end private def credit_card_params params . require ( :credit_card ) . permit ( :top_secret_token , :some , :credit_card , :attrs ) end end

Strong Opinion: Permit your params before mutating the object. This allows you to permit only the externally-provided parameters, then modify the object as you see fit.

class CreditCardsController < ApplicationController def update credit_card = CreditCard . find ( params [ :id ] ) assignable_params = credit_card_params assignable_params [ :top_secret_token ] = TopSecretToken . new ( args ) if credit_card . update ( assignable_params ) . . . else . . . end end private def credit_card_params params . require ( :credit_card ) . permit ( :some , :credit_card , :attrs ) end end

Defining permitted attributes multiple (sometimes many) times

With protected_attributes , the model provided a central place to specify which attributes were permitted for mass-assignment. With strong_parameters , permitted parameters are often defined multiple times for the same resource in different controllers. This is not DRY and it is not maintainable. Add in consideration for accepts_nested_attributes_for and you're maintaining countless lists of the same parameters.

Note: In the previous examples parameters were permitted directly in the controller as seen below. This was done for simplicity's sake, and also on the basis that no other controllers duplicated the same set of permitted parameters.

class DepartmentsController < ApplicationController def create department = Department . new ( department_params ) if department . save . . . else . . . end end private def department_params params . require ( :department ) . permit ( :some , :department , :attrs , employees_attributes : [ :some , :employee , :attrs ] ) end end module Special class DepartmentsController < ApplicationController def create department = Department . new ( department_params ) if department . save . . . else . . . end end private def department_params params . require ( :department ) . permit ( :some , :department , :attrs , employees_attributes : [ :some , :employee , :attrs ] ) end end end

Strong Opinion: Duplicated parameters should be permitted in a module that can be included wherever it is needed. Furthermore, nested attributes should not be redefined multiple times. Here's an example:

module Concerns module StrongParameters module Employee def employee_params params . require ( :employee ) . permit ( * self . permitted_attrs ) end def self . permitted_attrs %i(some employee attrs) end end end end module Concerns module StrongParameters module Department def department_params params . require ( :department ) . permit ( * self . permitted_attrs ) end def self . permitted_attrs [ :some , :department , :attrs , { employees_attributes : [ :id , :_destroy , * Concerns : : StrongParameters : : Employee . permitted_attrs ] } ] end end end end class DepartmentsController < ApplicationController include Concerns : : StrongParameters : : Department def create department = Department . new ( department_params ) if department . save . . . else . . . end end end module Special class DepartmentsController < ApplicationController include Concerns : : StrongParameters : : Department def create department = Department . new ( department_params ) if department . save . . . else . . . end end end end class EmployeesController < ApplicationController include Concerns : : StrongParameters : : Employee def create employee = employee . new ( employee_params ) if employee . save . . . else . . . end end end

Summary

We follow these practices at Hint, and it's helped us (and our clients) a lot. Does your organization need help wrangling strong parameters? We can help. Ping me on twitter: @benjaminwood.