Learning About XSS Attacks in Rails

| Comments

The emphasis is, make sure any user input is html escaped or sanitized before sending it to a browser.

If it isn’t, you risk exposing your users to malicious code delivered by your application. Potentially, the code could expose user session keys, cookies, or execute other types of attacks.

This post is inspired by railssecurity.com, a month long newsletter about Rails securities.

What is an XSS Attack?

Let’s take a look a few examples. First we need a user input field. I created a simple form where a user could create a profile and style it with HTML tags.

1
2
3
4
<%= form_for(@user) do |f| %>
  <%= f.label "Profile" %>
  <%= f.textarea :profile %>
<% end %>

Not HTML Escaped

I then put the following snippet into the text area and submitted the form.

1
Hey, nice forum!<script>alert("Guess who just got owned?")</script>

There are three ways we can deliver the contents of a users’s profile to the browser, let’s look at html escaped vs no escaping.

When not html escaped, the malicious code is executed when the page is loaded.

In the view:

1
<%= raw @user.profile %>

It renders as:

1
2
3
<p>
  Hey, nice forum!<script>alert("Guess who just got owned?")</script>
</p>

Screenshot:

We just completed an XSS attack. Congratulations!

HTML Escaped

Now let’s escape the html so that the javascript isn’t executed.

In the view:

1
<%= html_escape @user.profile %>

It renders as:

1
2
3
<p>
  Hey, nice forum!&lt;script&gt;alert(&quot;Guess who just got owned?&quot;)&lt;/script&gt;
</p>

Screenshot:

The javascript isn’t executed, and the symbols are rendered into a human readable format.

Default Behavior

Since a version of Rails 3, all strings by default are html escaped. Yeah! Great practice, because I’m pretty sure I’d easily forget to wrap a user inputted string with html_escape. For details and discussion, take a look at Yehuda Katz’s blog post on SafeBuffers.

When we call raw(@user.profile), we are setting html_escape to true for that string. This instructs Rails to display the raw contents of the string. By default, html_escape is set to false.

Because of the default behavior, we can safely do the following and know that it will always be html escaped.

1
<%= @user.profile %>

We could also turn off html escaping with the following.

1
<%= @user.profile.html_safe %>

Under the hood, raw is converting the object it receives to a string and then calling html_safe on it.

1
2
3
def raw(stringish)
  stringish.to_s.html_safe
end

By calling html_safe directly, we would want to be 100% confident that the object we will be receiving will never be nil. If html_safe, is called on nil, then we would receive a no method error. For example, if a user didn’t fill out their profile, then Rails would return to us nil object when we ask for the profile. Since nil doesn’t know about the method html_safe, it would create an application error.

To play it safe, get into the habit of using raw and you won’t have to worry about nil objects.

But I Want to Show HTML Tagsā€¦

In our case, we want the raw string so that users can add styles to their profile. For this situation, Rails has a sanitize method that takes a whitelist approach to tags.

List of allowed tags:

1
2
>> ActionView::Base.sanitized_allowed_tags
=> #<Set: {"strong", "em", "b", "i", "p", "code", "pre", "tt", "samp", "kbd", "var", "sub", "sup", "dfn", "cite", "big", "small", "address", "hr", "br", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "dl", "dt", "dd", "abbr", "acronym", "a", "img", "blockquote", "del", "ins"}>

In the view:

1
<p><%= sanitize @user.profile %></p>

It renders as:

1
<p>Hey, nice forum!</p>

Screenshot:

Because we sanitized the string returned by @user.profile and the <script> tag was not in the array of allowed tags, the tag is completely removed from the string before being delivered it to the browser.

Edge Cases

The sanitize method and default escaping covers scenarios when user inputs are being retrieved from within the Rails view; however, Ben from CodeClimate in a railssecurity.com lesson points out that another dangerous pattern is a method that returns an html template. A common place to find a method like this is in a Rails Helper.

1
2
3
4
5
module UserHelper
  def block_profile(user)
    "<div>#{user.profile}</div>".html_safe
  end
end

Because we want the div blocks, i.e. not html escaped, we call html_safe on the string so that the raw contents are returned by block_profile.

When we use block_profile(user) in the view, the contents of the string would be returned to us without being escaped thus exposing us to XSS attacks again.

Ben’s solution is the following:

1
2
3
4
5
module UserHelper
  def block_profile(user)
    "<div>#{h user.profile}</div>".html_safe
  end
end

The letter h is an alias for html_escape which we saw earlier. By wrapping user.profile in html_escape, we are explicitly escaping the the contents of user.profile, then instructing Rails that the raw contents of the string are safe to display.

My recommendation is to not use raw or html_safe in a Helper method. Instead, use the content_tag method which will automatically html escape the content it receives.

1
2
3
4
5
module UserHelper
  def block_profile(user)
    content_tag :div, user.profile
  end
end

Alternatives

There are other strategies, such as sanitizing user inputs before being saved to the database. There are gems that support these other strategies, this Stack Overflow answer is a place to start when checking out alternatives.

Take Home Points

References

Comments