Cross-site scripting (XSS) is an annoyingly pervasive and dangerous web vulnerability and Ruby on Rails applications are no exception.
- 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
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.
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.
In older versions of Rails, developers would need to manually escape every output, typically with the
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.)
Most templating libraries also provide a way of skipping escaping. ERB uses the double
This may be harder to catch in a manual code review.
While uses of
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.
html_safe, it is possible to introduce cross-site scripting into templates with unquoted attributes.
Consider this code:
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
When rendered, the code will be
When a victim mouses over the paragraph, the XSS payload will fire.
To be safe, always enclose attribute values with double quotes.
URLs in link_to
A lot of confusion has come up around Brakeman’s warnings about XSS in
The URL passed to
link_to (the second argument) will be HTML escaped. However,
link_to allows any scheme for the URL. That includes
For example, if a user can set their home page to any URL, then this becomes an XSS vector:
A user can set their home page to
When rendered, an attack might look like
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.
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
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:
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:
Should I use sanitize?
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!
(Wondering about SQL injection in Rails? Take a look at our Rails SQL injection guide.)