204: XSS Protection in Rails 3
(view original Railscast)
Way back in episode 27 [watch, read] we covered cross-site scripting. This is an important subject that anyone who develops for the web needs to understand. One of the places where web applications are vulnerable to XSS attacks is when they display input from users. It is therefore important to escape any user-generated input that is displayed and in Rails applications this is usually done by making use of the h
method.
<%= h comment.content %>
Using the h method to escape output.
In Rails 3, however, output is escaped automatically so there’s no need to put the h
method in your views and in this episode we’ll show you how Rails 3 handles the escaping of output.
To demonstrate output escaping we’re going to use a simple blogging application written in Rails 3. In this app we have articles and each article has a number of comments associated with it. To test how the comment system copes with an attempt at cross-site scripting we’ll enter <script>alert('I steal cookies!')</script>
in each field in the comments form and submit our evil comment.
When we add the comment and it is displayed on the page we’ll see that Rails 3 has automatically escaped the HTML tags in the fields that we submitted. Let’s take a look at how Rails does this.
The code that displays each comment is contained in a partial and if you look at the code you’ll see that none of the output is escaped with h
.
/app/views/comments/_comment.html.erb
<div class="comment"> <strong><%= link_to comment.name, comment.url %></strong> <p><%= comment.content %></p> </div>
In Rails 2 this would have meant that the alert messages would have been shown, but all of the output in the partial is being correctly escaped by Rails 3, even when passed through helper methods such as link_to so there’s no need to use the h method here.
What happens, thought, if we’re transitioning an application from Rails 2 and have our output escaped with h
? We can find out by escaping the output in the partial above and reloading the page.
/app/views/comments/_comment.html.erb
<div class="comment"> <strong><%= link_to h(comment.name), comment.url %></strong> <p><%= h comment.content %></p> </div>
When we reload the page we’ll see that it looks the same and the output hasn’t been double-escaped. Rails is clever here; even if we use the h
method it will escape the script tag only the once.
It might seem that the h method does nothing in Rails 3 but that’s not that case. It does still have a purpose and we’ll show you later what that is. But before we do that we’ll look at a related feature. While it’s great to have the output escaped automatically there might be times when we want to display the raw content. If we trust the content that the user enters, say they’re an administrative user, and we want to display exactly what they enter then we can use the new raw
method to do that.
<div class="comment"> <strong><%= link_to comment.name, comment.url %></strong> <p><%= raw comment.content %></p> </div>
If we reload the page now the JavaScript we entered in the comment field will be executed.
So, in Rails 3 whenever we don’t want content to be HTML escaped we can use the raw
method. But how does it work? Rails 3 seems to be pretty clever about when to escape content and when not to. We’ll show you now how that works.
We’ll demonstrate escaping in the console, which in Rails 3 we can invoke with the rails c
command.
$ rails c Loading development environment (Rails 3.0.0.beta) ruby-1.9.1-p378 >
Rails 3 has a concept of HTML-safe strings. This means that we can check if any string is safe to output as HTML by calling the new method html_safe?
on it.
> "foo".html_safe? => false
We can mark a string as being HTML-safe by calling the html_safe
method on it.
> safe = "safe".html_safe => "safe" > safe.html_safe? => true
No actual escaping goes on here. All that happens is that a boolean property is set against a string to determine whether it should be escaped before being output.
So how does this apply in our view template? Well, Rails looks at each piece of output and sees if it’s marked as HTML-safe. If it’s not then it will be automatically escaped when it is output to the view. If it is safe then it is passed through without being escaped. If we use the h
method to escape a string it will perform the escaping and mark the string as HTML-safe. This means that Rails 3 will see that the string is safe and not escape it again.
When the raw
method is used on a string it will be marked as HTML-safe but not escaped, ensuring that the string’s content is passed to the output without being changed.
This is an important thing to understand when you’re using helper methods. We’ll explain this by creating a helper method called strong
that wraps whatever is passed to it in <strong>
tags. We’ll use it in our template like this:
/app/views/comments/_comment.html.erb
<div class="comment"> <%= strong link_to(comment.name, comment.url) %> <p><%= raw comment.content %></p> </div>
We’ll create the strong
method in the ApplicationHelper
:
/app/helpers/application_helper.rb
module ApplicationHelper def strong(content) "<strong>#{content}</strong>" end end
When we reload the page, however, we’ll see that this hasn’t worked the way we wanted it to.
Rails 3’s automatic escaping has escaped the <strong>
tag here and this is because our helper method doesn’t create an HTML-safe string.
There are two simple rules that need to be need to be followed when working with helper methods that return HTML. Firstly, we need to ensure that any strings returned are marked as HTML-safe.
/app/helpers/application_helper.rb
module ApplicationHelper def strong(content) "<strong>#{content}</strong>".html_safe end end
This fixes the problem of the <strong>
tag being escaped but by doing that the content between the tags will not be escaped. We can solve this by escaping the content with h
:
/app/helpers/application_helper.rb
module ApplicationHelper def strong(content) "<strong>#{h(content)}</strong>".html_safe end end
So as long as we remember to escape all input with h
and then mark the resulting string as html_safe
it will be rendered properly. If we reload our comments page now we’ll see that the <strong>
tag hasn’t been escaped but that the content of the second comment, which includes the potentially dangerous JavaScript is escaped.
That’s it for this episode. The automatic escaping is a welcome addition to Rails 3’s views and it removes the need to remember to escape every piece of output with h
, this reducing the changes of your application being a victim of cross-site scripting.