Web application security is a must nowadays. There are many bots trying to exploit pretty much any website on the Internet, which means that even brand new websites can become victims of cyberattacks. Cross-site scripting, or XSS for short, is one such attack. XSS is a type of injection attack, meaning that the attacker injects malicious code into the application. That code then is executed on the victim’s machine.
XSS is one of the most common attacks, so you should make sure that your applications are safe. If you want to know more about XSS attacks in general, here’s a terrific overview. In this post, you’ll learn what XSS attacks look like in Ruby on Rails applications and how to prevent them.
How XSS Works
Let’s cover some basics first. You know already what XSS is, but what does it actually look like? In the most simple scenario, imagine you have a form on your website. You’re expecting the user to enter, for example, their name in the text field. But what if a user enters the following JavaScript code?
<script>alert("This is a XSS attack!")</script>
If, after submitting the form with that field, you see a popup in your web browser, it means that your application is vulnerable to an XSS attack. Of course, displaying a simple popup by itself doesn’t create any real risk; that was just an example. But imagine that an attacker writes JavaScript code that tries to steal other users’ credentials or hijack their sessions. That’s the real risk. An attacker can create code that downloads other users’ cookies and sends them to the attacker’s server. All of this will happen under the hood. The user won’t even notice.
XSS in Rails
The good news is that Rails has built-in XSS prevention. Before you get too excited, though, I need to tell you that it doesn’t prevent all XSS attacks. So read on.
To prevent XSS attacks, Rails automatically escapes all data sent from Rails to HTLM output. In the example above, we demonstrated that pasting JavaScript code into the text field can lead to XSS. This is because we use valid HTML tags for scripts (), so the victim’s browser interprets it as any other HTML code. It doesn’t know that this particular piece of code came from the attacker and not from the server.
Escaping HTML Tags
What does it mean when we say that Rails automatically escapes the HTML output? It means that Rails changes the known opening and closing tags and other special characters to something else. For example, the HTML tag opening character “<” is changed to “<”. Therefore, we no longer send valid HTML code to the browser. We send modified code. Without <> tags, the code won’t be executed by the browser as standard HTML code. You can generate a typical text input with the following Rails code (form helper):
<%= form.text_field :first_name %>
This allows a user to enter their first name in the text field. You can then use the user inputs later as follows:
<h2>Welcome <%= params[:first_name] %></h2>
If Rails didn’t protect you from XSS, and if we pass the previously mentioned JavaScript instead of the first name, the HTML output would look like this:
<h2>Welcome <script>alert("This is a XSS attack!")</script></h2>
This looks from the browser perspective like normal HTML code. Therefore, the browser would execute the code from script tags. But as we mentioned before, Rails escapes user input and changes the special characters that indicate HTML opening tags. So Rails-generated HTML output will look like this instead:
<h2>Welcome <script>alert("This is a XSS attack!")</script></h2>
Without proper HTML opening and closing tags, the browser won’t execute the JavaScript code (it will just print it like any other text). This covers roughly 80 percent of cases. However, there are ways to create XSS-vulnerable code with Rails.
Insecure by Design
There are ways to intentionally disable string escaping. You may want to pass raw data this way, such as when generating HTML by a third-party component or when offering rich text editing functionality. In these cases, you can use a raw helper as follows:
<%= raw @user.descripion %>
As the name suggests, this passes raw data to HTML output. Just be sure, if you do this, that you know what you’re doing because passing user input to the raw helper creates XSS vulnerabilities!
Another option when you need to disable Rails’ auto-escaping feature is the .html_safe method. There are legitimate use cases for it, but using it together with user input is not safe. Also, the name .html_safe doesn’t mean that the output will be secured. It means that whatever you pass using this method is safe to be interpreted as HTML.
<%= @user.description.html_safe %>
You need to double or triple check the code when you use one of the above. Without string escaping, you are prone to XSS vulnerabilities.
Hidden XSS
You know already that Rails automatically escapes data sent to HTML output. However, there are a few Rails helpers that don’t escape the data because they were never designed to receive user input. Unlike with raw or .html_safe, which purposely disable string escaping, these methods were not designed with that need in mind. Even so, some people pass user input to them anyway, which creates an XSS vulnerability. One example is the link_to helper. It’s very commonly used in Rails to create links, for example.
<%= link_to "User portfolio", user_portfolio_path %>
In most cases, we pass two parameters to the link_to helper: the name of the link and its destination, which usually is one of the Rails routes. However, there is nothing stopping you from hardcoding the destination URL as follows:
<%= link_to "User portfolio", "http://example.com" %>
And most important, there is nothing stopping you from providing a destination in the form of a variable that can come from user input:
<%= link_to "User portfolio", @user.portfolio_url %>
The above code is XSS vulnerable. Why? The user can provide JavaScript code as the portfolio URL. And since link_to wasn’t designed to be used with user-generated input, it doesn’t automatically do string escaping. So you won’t get a standard link like the following:
<a href="http://userexampleportfolio.com">User Portfolio</a>
Instead, you’ll get something like this:
<a href="javascript:alert('XSS Attack!')">User Portfolio</a>
Something similar may happen in other places where no one expects user input, such as the metadata field, background images, and comments. Every place that was not designed to receive user input may be prone to XSS. So you don’t want to end up with something like this in your code:
<td background="javascript:alert('User entered XSS code')"></td>
Make sure that you always pass user input as a parameter and that you sanitize it. And whenever possible, use the allowed values list.
Summary
As you can see, you don’t need to do anything special to protect yourself from XSS in Rails. The framework does most of the job for you. You just need to avoid passing user input when you’re not supposed to.
By design, there are a few helpers and methods that don’t include automatic data escaping, but they were never meant to be used with user-generated input. Your job is simply to make sure that you don’t pass that kind of input using these methods. If you want to learn more about securing Rails, we also published a Rails SQL Injection Guide.
This post was written by Dawid Ziolkowski. Dawid has 10 years of experience as a Network/System Engineer at the beginning, DevOps in between, Cloud Native Engineer recently. He’s worked for an IT outsourcing company, a research institute, telco, a hosting company, and a consultancy company, so he’s gathered a lot of knowledge from different perspectives. Nowadays he’s helping companies move to cloud and/or redesign their infrastructure for a more Cloud Native approach.