Cross-Site Scripting in Rails

Sep 8, 2017

Cross-site scripting (XSS) is an annoyingly pervasive and dangerous web vulnerability and Ruby on Rails applications are no exception.

The basic forms of XSS occur when an attacker can manipulate the HTML output of a website in order to execute malicious JavaScript in a victim’s browser. XSS can be used to do many things, including:

  • Read anything on the vulnerable page
  • Insert/remove content (ads are especially popular)
  • Steal cookies/sessions
  • Send requests on behalf of the victim
  • Access victim’s camera/location
  • Scan the victim’s network
  • …and much more!

Below, we will use code like alert(...) as sample XSS “attacks”. Of course alert boxes are not that scary, but we are just using it as a placeholder for arbitrary JavaScript that can contain a real attack.

If you want to see how dangerous XSS can be, check out the BeEF project. BeEF is a Sinatra app that allows you to control XSS victims’ browsers and launch many pre-made attacks.

To attack a victim with BeEF, all you need to do is get the victim to execute the BeEF “hook” code. Any victim that loads the “hook” code will show up in the BeEF command and control center as a browser you can attack. That is scary!

How does XSS even happen?

In the end, a web server provides HTML for a browser to render. That HTML is composed from many sources including, potentially, an attacker. Anywhere a site accepts external input can become a vector for XSS payloads: usernames, comments, reviews, bios, search queries, etc. But not just input forms! Query parameters, headers, cookies - anything the attacker can send to the server could be a source of XSS.

Once the payload is sent to the server, the server must incorporate the payload into the HTML sent back to a victim. The most common case is simply dropping the payload into an HTML template and rendering as part of the page. It’s also possible to grab values query parameters or AJAX responses and unsafely add them to the DOM via JavaScript.

Okay…But how does XSS happen in Rails?

To prevent XSS, user-controlled values should use a context-appropriate escaping/encoding. For the majority of scenarios, this is HTML escaping (i.e. < becomes &gt;).

In older versions of Rails, developers would need to manually escape every output, typically with the h method:

<%= h params[:query] %>

In Rails 3, templates escaped output by default. Hooray!

Sadly, Rails 3 also introduced the unfortunately named html_safe method to bypass this escaping. Quite a few people have been confused into thinking html_safe makes strings safe. What it really does is mark the string as “safe” so that it will not be escaped. (The raw method does the same thing.)

Happy XSS!
<%= params[:query].html_safe %>

Most templating libraries also provide a way of skipping escaping. ERB uses the double ==:

<%== params[:query] %>

This may be harder to catch in a manual code review.

While uses of html_safe or raw in templates is pretty obvious, XSS often sneaks in when building HTML strings inside view helpers. The returned HTML will have html_safe called on it, since it is intended to be rendered as HTML. But if user input is passed to the helper and the helper doesn’t escape it, there will be potential for XSS again.

Unquoted attributes

Even without html_safe, it is possible to introduce cross-site scripting into templates with unquoted attributes.

Consider this code:

<p class=<%= params[:style] %>...</p>

An attacker can insert a space into the style parameter and suddenly the payload is outside the attribute value and they can insert their own payload. An simple attack could be something like x%22onmouseover=javascript:alert(‘xss!’).

When rendered, the code will be

<p class=x onmouseover=javascript:alert("xss!") >...</p>

When a victim mouses over the paragraph, the XSS payload will fire.

To be safe, always enclose attribute values with double quotes.

A lot of confusion has come up around Brakeman’s warnings about XSS in link_to.

The URL passed to link_to (the second argument) will be HTML escaped. However, link_to allows any scheme for the URL. That includes javascript: and data: which can be used to execute JavaScript.

For example, if a user can set their home page to any URL, then this becomes an XSS vector:

<%= link_to "My Cool Site", @user.home_page %>

A user can set their home page to javascript:... or a dangerous data: payload.

When rendered, an attack might look like

<a href="javascript:prompt('xss!')">My Cool Site</a>

When a victim clicks the link, the JavaScript will execute.

To avoid this vulnerability, always validate URLs when accepting them from outside sources. In particular, it should be possible to limit URL schemes to http/https in nearly all cases.

JSON in templates

Rendering JSON inside of HTML templates is tricky. You can’t just HTML escape JSON, especially when inserting it into a script context, because double-quotes will be escaped and break the code. But it isn’t safe to not escape it, because browsers will treat a </script> tag as HTML no matter where it is.

The latest versions of Rails will properly encode JSON with Unicode sequences. HTML special characters will be converted to \u… which is meaningless to browsers but will parse correctly as JSON.

The Rails documentation recommends always using json_encode just in case to_json is overridden or the value is not valid JSON.

  var userdata = <%= raw json_encode(@stuff.to_json) %>

In older versions of Rails, the keys in JSON hashes were not escaped. In even older versions, the ActiveSupport.escape_html_entities_in_json setting was ignored. It’s important to stay up-to-date!

Inline renders - even worse than XSS!

First, be careful when using render text: ...! By default it actually sets the content-type to text/html, so any user input in the rendered string will be an XSS opportunity. Starting in Rails 4.1 text is deprecated and it is recommended to use plain instead.

More importantly, be very, very careful when using render inline: …. The value passed in will be treated like an ERB template by default.

Take a look at this code:

render inline: "Thanks #{}!"

Assuming users can set their own name, an attacker might set their name to <%= rm -rf / %> which will execute rm -rf / on the server! This is called Server Side Template Injection and it allows arbitrary code execution (RCE) on the server.

If you must use an inline template (why??) treat all input the same as you would in a regular ERB template:

render inline: "Thanks <%= %>"

Should I use sanitize?

Only ever use the sanitize method if you are intending to allow some HTML tags. The sanitize helper has had several vulnerabilities over the years.

Stripping HTML tags correctly and safely in all cases is an extremely difficult problem to get right and sanitizers need to be updated frequently to stay on top of new tags. Escaping HTML is actually pretty easy and is a much safer approach.

Brakeman Pro can help!

This post has shown just a few ways XSS can infiltrate Rails applications. Despite being a well-understood vulnerability, cross-site scripting remains the most common web vulnerability and can be tricky to eradicate.

Fortunately, Brakeman Pro can look deep into your code to find instances of unescaped output, unsafe view helpers, inline templates, unquoted attributes, and more!

Have questions about Brakeman Pro? We are happy to help!

(Wondering about SQL injection in Rails? Take a look at our Rails SQL injection guide.)