Rails’ safety mechanisms
Jason Charnes
Jason Charnes
12/12/2022
Engineering8 min read

Rails’ safety mechanisms

Companies build products with Ruby on Rails because of its focus on efficiency. The framework comes with a set of defaults that allows you to focus on building, not configuring.

Defaults can have limits. We can often find ourselves with paper cuts if we’re not careful. Fear not! Rails has conventions for this, too. Let’s look at some tools the framework gives us to protect ourselves.

Mass assignment protection in Rails#

One of the first things Rails developers learn is handling form parameters. The params object contains all the form parameters for us to use in controllers and views. But whenever Rails generates a scaffold it uses something called strong parameters.

What are strong parameters, exactly? When creating or updating an Active Record model we can pass an optional hash of attributes.

Ruby
User.create(name: "Jason Charnes", location: "Memphis")`

This pairs beautifully with the Rails form helpers:

<%= form_with model: @user do |form| %>
  <%= form.text_field :name %>
  <%= form.text_field :location %>
  <%= form.submit %>
<% end %>

The parameters sent from the form to the controller look like this:

Ruby
{user: {name: "Jason Charnes", location: "Memphis"}}

We can pass the user hash directly:

Ruby
class UsersController < ApplicationController
  def create
    User.create!(params[:user])
  end
end
Warning
🚨 This is mass assignment; it’s dangerous.

While creating the record, we’re mass assigning the attributes. This is simple, clean, and appears to work great, but there’s a problem.

The User model also has an admin boolean that defaults to false. What if our end-user is sneaky and adds this to the form?

HTML
<input type="hidden" name="user[admin]" value="true" />

Now the params look like this:

Ruby
{user: {name: "Jason Charnes", location: "Memphis", admin: "true"}}

We’re passing the entire list of parameters to Active Record, including admin.

Ruby
class UsersController < ApplicationController
  def create
    @user = User.create!(params[:user])
    @user.admin? #=> true
  end
end

Yikes, we gave away the keys to the kingdom. This is the role strong parameters play.

Strong parameters

Instead of mass assignment (passing the raw params directly to Active Record), we use strong parameters to signal which attributes are allowed.

Ruby
class UsersController < ApplicationController
  def create
    User.create!(user_params)
  end

  private

  def user_params
    params.require(:user).permit(:name)
  end
end

Here, we define a user_params method that is responsible for allowing specific parameters through.

We start by telling the params object that we require the parameters to have a key of :user. If the params don’t have this key, an error is raised and the request is halted.

From there we provide a list of permitted attributes to the permit method.

Now, if our end-user tries to pass admin as a parameter, Rails will exclude it from the list of params passed to Active Record.

Rails will log unpermitted parameters by default. This is useful for debugging in development and looking for bad actors in production. If you want to take things further, you can tell Rails to raise an error if unpermitted parameters are passed:

Ruby
config.action_controller.action_on_unpermitted_parameters = :raise

Strong parameters are a controller-level feature. Is there a way to enforce this in the model? Rails doesn’t provide this, but it used to.

Before the introduction of strong parameters in Rails 4, mass assignment protection happened at the model level. (Does anyone remember attr_protected and attr_accessible?!)

The strong parameters pattern is more flexible. You likely want to permit different attributes depending on the context. A user shouldn’t set their admin status. But an existing admin may be able to set it.

While it’s easier to define it in the model, it’s simpler to let the controller do it.

N+1 prevention in Rails#

N+1 queries are unfortunately easy to perform in Active Record. ✨

If we access an association we haven’t loaded, Rails will make the database lookup on our behalf. Rails has our back here. It comes at a cost, though. (Unless you view N+1s as a feature.)

Pretend we’re rendering a list of orders:

Ruby
class OrdersController < ApplicationController
  def index
    @orders = Order.all
  end
end
<% @orders.each do |order| %>
  <tr>
    <td><%= order.id %></td>
    <td><%= order.customer.name %></td>
    <td><%= order.created_at %></td>
  </tr>
<% end %>

We’re rendering the customer’s name on each order. An order belongs to a customer.

Ruby
class Order < ApplicationRecord
  belongs_to :customer
end

We didn’t ask the database for customers, though. Just orders. The logs show the single database query for orders. ✅

SQL
SELECT "orders".* FROM "orders"

And a customers table query for each order we rendered. ❌

SQL
SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 LIMIT $2  [["id", 3], ["LIMIT", 1]]
SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 LIMIT $2  [["id", 4], ["LIMIT", 1]]
SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 LIMIT $2  [["id", 5], ["LIMIT", 1]]
SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 LIMIT $2  [["id", 6], ["LIMIT", 1]]
SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 LIMIT $2  [["id", 7], ["LIMIT", 1]]
SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 LIMIT $2  [["id", 8], ["LIMIT", 1]]
SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 LIMIT $2  [["id", 9], ["LIMIT", 1]]
SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 LIMIT $2  [["id", 10], ["LIMIT", 1]]
SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 LIMIT $2  [["id", 11], ["LIMIT", 1]]
SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 LIMIT $2  [["id", 12], ["LIMIT", 1]]
SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 LIMIT $2  [["id", 13], ["LIMIT", 1]]
SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 LIMIT $2  [["id", 14], ["LIMIT", 1]]
SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 LIMIT $2  [["id", 15], ["LIMIT", 1]]

Rails knows customers aren’t loaded, so it lazy loads (does a database lookup for) each customer on the fly.

The query returns 15 orders, which means 15 individual customer queries. This is what I mean by N+1. We have N order records. For every order, we have +1 more query to look up the customer.

This is a problem when working with large datasets.

Each request to the customers table isn’t necessarily a bottleneck — they’re pretty quick. But they add up. And sometimes it’s not just one association per record. It’s many associations per record.

The solution to N+1s

We fix this by preloading associations.

Ruby
class OrdersController < ApplicationController
  def index
    @orders = Order.includes(:customer).all
  end
end

By adding the .includes method with the association we want to preload, Rails ensures that every corresponding customer is loaded.

Instead of 15 extra SQL queries, we only have 1 extra query:

SQL
SELECT "orders".* FROM "orders"

SELECT "customers".* FROM "customers" WHERE "customers"."id" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)  [["id", 1], ["id", 2], ["id", 3], ["id", 4], ["id", 5], ["id", 6], ["id", 7], ["id", 8], ["id", 9], ["id", 10], ["id", 11], ["id", 12], ["id", 13], ["id", 14], ["id", 15]]

Over time it’s easier to know when you’re introducing an N+1 query, but it’s still easy to miss them. Let’s look at how we can catch N+1s before shipping to production.

Before Rails 6.1

The Bullet gem is the tool for discovering N+1s.

Bullet keeps an eye out for N+1 queries in your application. From Rails logs to notifying you in Slack, it has many ways to warn you of N+1 queries.

Remember, it’s still up to you to listen and fix the N+1s. 😅

Rails 6.1+

If you don’t want to fool with another dependency or know that you’ll just ignore Bullet warning you of N+1s, there’s a more invasive option.

Rails 6.1 introduced a mechanism for detecting down N+1s: strict_loading.

Ruby
class Order < ApplicationRecord
  belongs_to :customer, strict_loading: true
end

Adding this option to the customer association results in an ActiveRecord::StrictLoadingViolationError raised if Rails detects you’re lazy loading the association.

When we encounter this error, it’s clear to us what happened:

`Order` is marked for strict_loading. The Customer association named `:customer` cannot be lazily loaded.

Adding this option to each association is tedious! Luckily, there’s a way to enable this functionality globally.

Applying strict loading in development

If you only want an error raised in development, not production, you can enable the option in config/development.rb

Ruby
config.active_record.strict_loading_by_default = true

I like Aaron Francis’ idea of enforcing strict loading in development only. In our companion article on Laravel’s safety mechanisms, he suggests:

Lazy loading relationships does not affect the correctness of your application, merely the performance of it. Ideally, all the relations you need are eager loaded, but if not, it simply falls through and lazy loads the required relationships.

For that reason, we recommend disallowing lazy loading in every environment except production. Hopefully, all lazy loads will be caught in local development or testing, but in the rare case that a lazy load makes its way into production, your app will continue to work just fine, if a bit slower.

Applying strict loading regardless of the environment

If performance is critical and it’s important to raise this error in all environments, including production, apply the option in config/application.rb:

Ruby
config.active_record.strict_loading_by_default = true

Asynchronous association destruction#

Active Record provides a mechanism for deleting associated records when a parent record is deleted.

The quickest, simplest way to delete dependent data is by providing the dependent: :delete_all option on the association:

Ruby
class Order < ApplicationRecord
  has_many :invoices, dependent: :delete_all
end

Deleting this order will execute a separate, single SQL query to delete all the associated invoices.

Sometimes, though, the dependent records have “on delete” callbacks that need to run. Because delete_all uses a SQL query, it’s not going to instantiate the dependent records and run the delete callbacks.

To ensure the callbacks are run, we’d change the dependent option to destroy:

Ruby
class Order < ApplicationRecord
  has_many :invoices, dependent: :destroy
end

Deleting an order now instantiates each dependent, and associated record and calls #destroy on it.

But what happens if there are tens, hundreds, or even thousands of associated records? It means tens, hundreds, or even thousands of object instantiations and SQL calls.

This long-running functionality feels like something that could run in a background job. As it turns out, Rails agrees.

Changing the dependent: :destroy to dependent: :destroy_async will enqueue a background job to destroy the dependent, associated records.

It’s worth noting this only works for associations that do not have a foreign key constraint. For more options for destroying dependent relations, take a look at the Miss Hannigan gem.

We have another post on deleting data at scale with Rails if you want to dig in deeper.

Fail loudly, fail proudly#

Active Record methods fail loudly or silently.

You can often tell if an Active Record method fails loudly or silently by its name. Most (not all) methods that end with a bang (!) will raise an error. What’s the difference?

Loud failures

Loud failures raise an error and halt code execution.

A good example of this is Active Record’s .find method which locates the database record by ID. Trying to find a record that doesn’t exist raises an ActiveRecord::RecordNotFound error.

Ruby
class OrdersController < ApplicationController
  def update
    @order = Order.find(params[:non_existent_id]

    @order.update(order_params)
  end
end

We never get to the update call because the lookup raises an error and halts the request.

Silent failures

Instead of using .find, replace it with .find_by, which takes a list of attributes to look up instead of an ID.

Ruby
class OrdersController < ApplicationController
  def update
    @order = Order.find_by(id: params[:non_existent_id]

    @order.update(order_params)
  end
end

In this case, .find_by returns nil. This means we’ll get to the #update action, but it will try to call update on nil, which will raise the ever-present “undefined method x for nil.” 🥶

Comparing the two

In this example, we create an order and notification. Once we’re done creating records, we send a notification email.

Ruby
order = Order.create(order_params)

notification = Notification.create(notifiable: order)

NotificationEmail.deliver_later(notification)

What happens if creating the notification fails validation?

We get an instance of Notification back that isn’t in the database. The notification is passed to the NotificationEmail and sent in the background.

This block of code doesn’t fail. But when the job runs later, in the background, it fails. The notification can’t be retrieved from the database because it was never saved to the database. 😅

Silently failing

If we acknowledge it’s okay for a notification to fail, we can continue using the silent failure, create, and add a boundary:

Ruby
notification = Notification.create(notifiable: order)

if notification.valid?
  NotificationEmail.deliver_later(notification)
end

Failing loudly!

But often times I don’t expect code to fail. If it is failing, it’s a signal of a larger problem that should be addressed.

In our example, the background job failing wasn’t the root issue. It was a side effect. If we don’t expect creating a notification to ever fail, failing loudly might be a good option.

In this example, failing loudly is done by using create!:

Ruby
notification = Noficiation.create!(notifiable: order)
Notification.deliver_later(notification)

This juicy code block raises an error if validation fails and makes it easier to track.

If the job is failing because the notification wasn’t created, we have to figure out what enqueued the job and why it failed.

If an error is raised because the Notification failed to create, the job is never enqueued and we immediately know where to look to fix the problem. The error itself signals what went wrong.

Credential protection in Rails#

I once got a bill from AWS for $20,000+. Want to guess what happened?

My private git repo was compromised where I had hard-coded AWS credentials. That day I learned about the importance of keeping credentials out of your code.

We often store credentials as environment variables. Whenever we add a new credential, we update all the places our application runs: production, CI, the password manager team shares, etc.

What if I told you Rails has a built-in mechanism for securely storing credentials?

Rails credentials

Instead of keeping environment variables in sync across multiple developers and platforms, we can store our credentials securely in our application.

To get started, run the Rails CLI:

Terminal
bin/rails credentials:edit --environment=development

The first time this command is run it creates two files:

  1. config/credentials/development.yml.enc
  2. config/credentials/development.key

The first file is an encrypted YAML file. When we ran our command, the file was temporarily decrypted and opened in our editor for us to edit.

While the file is decrypted, we can update it to store our credentials in a standard YAML key/value structure:

Ruby
stripe:
  secret_key: SK1234
  publishable_key: PK1234

Once we close the file, it’s re-encrypted. The file can be decrypted only with the key it was encrypted with, which was the second file created. Rails adds the decryption keys to .gitignore, keeping it from accidentally being committed to git.

Once our secrets are saved, we can access them in the app using the following:

Ruby
Rails.application.credentials.stripe.secret_key
Rails.application.credentials.stripe.publishable_key
Warning

🚨 Keep the key that Rails generated safe! If you lose it, you’ll lose access to your credentials. If you work on a team, keep it somewhere safe, like a password manager.

Multiple environments

In the example above, I used the development environment. Rails is going to automatically defer to the development set of credentials in development.

I create a credentials file for each environment: development, staging, and production. This makes it easy to keep development and production credentials separate.

Development emails in Rails#

What’s more fun than using real email addresses in development? Accidentally sending emails to those real email addresses in development. 😎

(While we’re here… please don’t send emails from Active Record callbacks.)

While tools exist to make this experience better (Letter Opener or Mailtrap), Rails provides a few mechanisms to help.

Turn off email delivery

This is the least exciting yet most effective mechanism for preventing unwanted emails from being sent. To turn off email delivery, add the following setting to config/development.rb:

Ruby
config.action_mailer.perform_deliveries = false

This option may be suitable if you have email previews and a solid test suite.

Change the delivery method

Maybe you want to have a record of the email being sent for debugging. Action Mailer can save “sent” emails as files instead of delivering them.

To do this, add the following setting to config/development.rb:

Ruby
config.action_mailer.deliver_method = :file

Any emails “sent” from the application in development will be saved to tmp/mails. This saves the raw output, which might be enough for your use case.

For me, though, I typically want to see the email in its final, table-loaded, CSS-less, 1990s HTML email form.

Email interceptors

The final option we’ll look at is intercepting all emails sent in development and rerouting them to your email address. This only works if you have an active SMTP server or email provider configured in development mode.

We do this by defining an email interceptor:

Ruby
class DevelopmentEmailInterceptor
  def self.delivering_email(message)
    message.to = ["jason@example.com"]
  end
end

The email interceptor implements the .delivering_email method. Inside the method, we’ll change the message’s recipient to our email address.

To wire up the interceptor, we add the following option to config/development.rb:

Ruby
config.action_mailer.interceptors = ["DevelopmentEmailInterceptor"]

Now, any email sent from development will be rerouted to our email, no matter who it was initially addressed to.

Stay safe#

These tools give us more confidence in building our applications.

Having Rails raise an error every time you forget to include an association may feel like a minor annoyance. But it’s less annoying than having to revisit code a few months later to fix a performance issue a preload would have avoided.

Having to add the boilerplate strong parameters requires may feel boring. But give me boring over the excitement of a bad actor creating a security incident for my customers.

Go enjoy the vast magic of Rails, my friends.

Want a database with equally powerful built-in protections as Rails?