Rails became popular and still heavily used by enterprises for its focus on efficiency and productivity. It comes with most suitable defaults for each environment for developer to more focus on building than configuring.
Here we would look at few default relating to #CyberSecurity and how to override them to build a robust Cyber Security defence
Mass assignment protection
Rails, when build via scaffolding automatically white-lists all parameters that can be accepted via strong parameters. As SQL injection is still the most widely used attack till this day [OWASP Threats]
By whitelisting form parameters Rails ensures that only parameters allowed are one that are whitelisted from params
object
What are strong parameters, exactly? When creating or updating an Active Record model we can pass an optional hash of attributes.
Account.create(name: "Pankaj B", address: "Some Address")
The parameters sent as request payload from the form to the controller look like following
{account: {name: "Pankaj B", location: "Some Address"}}
These parameters can be passed directly
class AccountController < ApplicationController
def create
Account.create!(params[:account])
end
end
This is mass assignment and can be dangerous. As payload can be tempered with to pass on additional information.
If request payload is tempered with and passed on additional information. Think of admin
attribute; that is part of Account
model; so end user can temper and pass additional attribute and thus assigning self to be an admin, by passing following Payload
{account: {name: "Pankaj B", address: "Some Address", admin: "true"}}
Same are passed directly to Account.create
function and voila, this account now has admin privileges
class AccountsController < ApplicationController
def create
@account = Account.create!(params[:account])
@account.admin? #=> true
end
end
Yikes, we gave away the keys to the admin privileges. This is the role strong parameters play.
Strong parameters
Instead of mass assignment (passing the raw params directly to Active Record), Rails uses strong parameters to signal which attributes are allowed.
class AccountsController < ApplicationController
def create
Account.create!(account_params)
end
private
def account_params
params.require(:account).permit(:name)
end
end
Here, we define a account_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 :account
. 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-account 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 by following configuration changes
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.
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+1s are 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:
class OrdersController < ApplicationController
def index
@orders = Order.all
end
end
<% @orders.each do |order| %>
<tr>
<td><%= order.id %></td>
<td><%= order.account.name %></td>
<td><%= order.created_at %></td>
</tr>
<% end %>
We’re rendering the account’s name on each order. An order belongs to a customer.
class Order < ApplicationRecord
belongs_to :account
end
We didn’t ask the database for accounts, though. Just orders. The logs show the single database query for orders.
SELECT "orders".* FROM "orders"
And a accounts table query for each order we rendered.
SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]]
SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT $2 [["id", 3], ["LIMIT", 1]]
SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT $2 [["id", 4], ["LIMIT", 1]]
Rails knows accounts aren’t loaded, so it lazy loads (does a database lookup for) each account on the fly.
The query returns 4 orders, which means 4 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 accounts 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.
class OrdersController < ApplicationController
def index
@orders = Order.includes(:account).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 4 extra SQL queries, we only have 1 extra query:
SELECT "orders".* FROM "orders"
SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" IN ($1, $2, $3, $4) [["id", 1], ["id", 2], ["id", 3], ["id", 4]]
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
.
class Order < ApplicationRecord
belongs_to :account, strict_loading: true
end
Adding this option to the account 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 Account association named :account
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
config.active_record.strict_loading_by_default = true
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
:
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:
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:
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.
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.
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.”
Comparison
In this example, we create an order and notification. Once we’re done creating records, we send a notification email.
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:
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!:
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
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:
bin/rails credentials:edit --environment=development
The first time this command is run it creates two files:
config/credentials/development.yml.enc
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:
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:
Rails.application.credentials.stripe.secret_key
Rails.application.credentials.stripe.publishable_key
Note 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
Testing or building email functionality in development have been a big part of developers worry. What’s more painful is using real addresses with SMTP server and sending them again and again and waiting on it
Worry not, Rails provides a mechanism to help with this. Consider following options
Turn off deliveries
This configuration option is mostly easy to implement. Once implemented, it prevents mails from getting delivered in test/development environment. To turn off mail deliveries, head to config/development.rb
and add following settings
config.action_mailer.perform_deliveries = false
Note: that one should have solid test suite against this or email preview gems
Change the delivery method
A developer may choose to see how and what mail is being sent for debug/test purpose. ActionMailer have option for email to sent to file instead of delivering them. To do this, add the following setting to config/development.rb
config.action_mailer.deliver_method = :file
Now, emails sent from the application in developent environment will be diverted and saved under tmp/mails
. Developer may sneak peek into it for raw mail output as-is
Email interceptors
But what about if one want to see how it looks as actual mail in inbox. Relax; rails allows us to intercept email and re-route them to any email address. But for this to work one must have SMTP server and mail configuration in-place for application; because rails now will send actual emails while changing to:
address to one chosen. Consider following
class EmailInterceptor
def self.delivering_email(message)
message.to = ["xyz@example.com"]
end
end
The email interceptor implements .delivering_email
method, to change to:
address to reroute all emails to one address as chosen. Then we need to one more thing, to tell Rails to use interceptor we developed in config/developent.rb
config.action_mailer.interceptors = ["EmailInterceptor"]
Now, any email sent from development will be rerouted to that email, no matter who it was initially addressed to.
Third Party gems
There are many rubygems out there to intercept mail and show them locally. Few names are Letter Opener and Mailtrap
Keep your app safe
These tools gives developers more confidence in building our applications. Having Rails raise an error every time developer 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.
Using strong parameters are may feel a bit extra work, however it smoothes journey ahead where is developer have one less worry to worry about.
Believe me; cyber attacks are very common, its only time by when a attacker zeroes on app you are developing.
Keep practicing these cool inbuilt safety mechanism and thank me later.
About The Author
I am Pankaj Baagwan, a System Design Architect. A Computer Scientist by heart, process enthusiast, and open source author/contributor/writer. Advocates Karma. Love working with cutting edge, fascinating, open source technologies.
To consult Pankaj Bagwan on System Design, Cyber Security and Application Development, SEO and SMO, please reach out at me[at]bagwanpankaj[dot]com
For promotion/advertisement of your services and products on this blog, please reach out at me[at]bagwanpankaj[dot]com
Stay tuned <3. Signing off for RAAM