Elaina Polson

Musings of a Beginner Programmer

Nested Attributes, Strong Parameters, and Escaping the Threat of Code Injection

If you’re building applications in which you allow users to input data, you’ll undoubtedly have to deal with parameters at some point. Parameters are a way to access data sent in by the user, either as part of the URL or through an HTML form.

For example if you had an application that allowed users to create a new user profile, your HTML form might look like this:

1
2
3
4
5
6
7
<%= form_for(@user) do |f| %>
    <%= f.label :name %>
    <%= f.text_field :name %>
    <%= f.label :email %>
    <%= f.text_field :email %>
    <%= f.submit Create New User %>
<% end %>

The code above represents a form building object, created by the form_for method. It binds a form to a model object – in this case, to a user. The name and email address attributes passed to form_for by the user set the keys used in params to access the form’s values. Since the model object we’re creating is a user, params[:user] will be a hash with the keys :name and :email.

In order to save these attributes to a new user object upon form submission, you might have some code in your controller that looks like this:

1
2
3
def create
  @user = User.new(params[:user])
end

The lines of code above utilize something called mass assignment. It takes all of the values submitted by the user in the HTML form and assigns them as attributes to a user object, all at once. It’s much more efficient than writing this:

1
2
3
4
5
6
def create
  @user = User.new
  @user.name = params[:user][:name]
  @user.email = params[:user][:email]
  @user.save
end

But while mass assignment may seem optimal, there are risks associated with it – namely, something called code injection.

When you mass assign variables without actually specifying what is or isn’t allowed into your database, you are making room for a clever hacker to “inject” some kind of invalid input. The most innocent of hackers might want to simply add a new column name to a table, which would be easy enough to do when you’re allowing anything through your params. But the most malevolent of hackers could wreak havoc. In the worst cases, code injection can lead to data loss or corruption or unauthorized access to a system. We always want to protect our applications from this.

To escape the threat of code injection, we must use something called strong parameters in our applications. This involves “whitelisting” our parameters in a private method in the action controller so that only certain attributes are allowed to be passed through to the params hash. This allows you to specify exactly which attributes you want to permit for mass assigning.

The code below allows a user to input only two specified variables as keys in the params hash. Anything else will be filtered out.

1
2
3
4
private
def user_params
   params.require(:user).permit(:name, :email)
end

The require method identifies the required parameter, which is our user object. The permit method identifies the list of allowed parameter keys.

Then, in your controller, you could mass assign the whitelisted params, like so:

1
2
3
def create
   @user = User.new(user_params)
end

Great! Now we have effectively sanitized our user input. But wait – what if a user has multiple email addresses? How do we allow for nested attributes in our whitelisted params? It’s easier than it sounds! We just need to lay some basic groundwork.

First, let’s go to our model and add the accepts_nested_attributes_for method for emails. This creates an emails_attributes= method on our User model, and sets us up nicely for creating a nested form.

1
2
3
4
5
6
7
8
class User < ActiveRecord::Base
  has_many :emails
  accepts_nested_attributes_for :emails
end

class Email < ActiveRecord::Base
  belongs_to :user
end

Now, let’s edit the form on the create new user page. Fields_for is a method that will help us add nested attributes to our params hash.

1
2
3
4
5
6
<%= f.fields_for :emails do |emails_form| %>
        <%= emails_form.label :type %>
        <%= emails_form.text_field :type %>
        <%= emails_form.label :address %>
        <%= emails_form.text_field :address %>
<% end %>

But there’s something just a little annoying about fields_for. It renders its block once for every element of the association – in this case, for every email address of a user. But if a user has no email address (because she hasn’t added one yet), the block will render nothing. To get around this, we need to do a little housekeeping in our controller and build one or more empty “children” so that at least one block is shown to the user. The code below would result in 2 sets of email fields being rendered on the new user form.

1
2
3
4
def new
  @user = User.new
  2.times { @user.emails.build }
end

Now that our form is set up appropriately, a user can easily fill out a form with two email addresses! When they submit the form with the fields filled out appropriately, our parameters might look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  user => {
    'name' => 'Elaina Polson',
    emails_attributes => {
      '0' => {
        'type' => 'School',
        'address' => 'elaina@flatironschool.com'
      },
      '1' => {
        'type' => 'Personal',
        'address' => 'elaina@hotmail.com'
      }
    }
  }
}

As you can see, the emails_attributes key points to a hash, containing an id key, which points to another hash containing our attribute keys – which in this case, are type and address. So how do we allow for this nested data structure to pass through our params and update a new user accordingly? You guessed it – let’s head over to the controller and update our strong params! The following code will allow for our nested attributes to flow easily through our permitted params:

1
2
3
4
private
  def person_params
    params.require(:person).permit(:name, emails_attributes: [:id, :type, :address])
  end

And that’s all there is to it! Now we’ve sanitized our user input, allowed for nested attributes, and are ready to get to work building out the rest of our application.

Sources: Edge Guides Easy Active Record