Preventing Cross-site Scripting Vulnerabilities When Developing Ruby on Rails Web Applications
This article uses examples to explain how to develop secure web applications in Ruby on Rails that are not vulnerable to cross-site scripting vulnerabilities.
Your Information will be kept private.
Your Information will be kept private.
Table Of Contents
- What is HTML Escaping
- html_safe and Introduction to Safe Buffers
- Transferring Data from Rails to HTML
- Transferring Data from Rails to JavaScript
- Transferring JSON Data to HTML/JavaScript
- Final Notes
- Vulnerability Classification and Severity Table
Cross-site scripting is a very common injection type of web application vulnerability. It allows attackers to inject any type of client-side script which is then executed by the victims’ browsers in their browsing contexts. Typically cross-site scripting attacks are used to bypass access controls and to steal web sessions. Read our article What is Cross-site Scripting for more technical details on this notoriously known vulnerability.
Ruby on Rails has a built-in XSS protection mechanism which automatically HTML escapes all the data being transferred from Rails to HTML. While this is a big plus for Rails framework security, it is not enough to solve all XSS problems, which is why every day new cross-site scripting vulnerabilities are still being discovered on Ruby on Rails web applications.
This article explains several different techniques which you can use to develop Ruby on Rails web applications that are not vulnerable to cross-site scripting (XSS).
What is HTML Escaping
In programming/scripting languages, there are some special characters that help interpreters / parsers to differentiate data from actual code. In HTML, tags start and finish with the < > characters. In the tags, we differentiate attributes from data by putting the latter between single or double quotes.
Those special characters might be a part of the data that is needed to be displayed within a HTML page. In this case those characters must be substituted with innocuous ones to make sure they are not being considered as regular HTML tags.
Rails uses ERB::Util#html_escape function to escape HTML entities. This particular function does the substitution based on the following hash (more about ERB::Util).
{ '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' }
Here is an example:
<b>Hello <%= params[:name] %></b>
If the above is attacked with ?name=<script>alert(1)</script>, the output will be as follows:
<b> Hello <script>alert(1)</script>gt; </b>
As can be seen in the generated HTML code, all the special characters have been escaped.
html_safe and Introduction to Safe Buffers
Sometimes, Rails’ built-in auto escaping feature might not be desirable because of some business need, and actually we may intentionally want to output data that contains legit HTML through views. There are several ways how to disable this automatic escaping. For e.g. the data can be marked as html_safe as shown in the below example:
Hello <%=params[:name].html_safe%>
If the above is attacked with the ?name=<script>alert(1)</script> query string, the result will be:
Hello <script>alert(1)</script>
What’s happening here is that when html_safe is being called on a String and as a response, an ActiveSupport::SafeBuffer object is being created and returned (ref). SafeBuffer objects in the views are not escaped as they are already considered “safe”. Since it is really easy to introduce XSS vulnerabilities through the use of html_safe, it must be only used for trusted/validated data with utmost care.
In this following Rails view, params[:input] will be HTML-encoded before appending into other SafeBuffer, therefore it would be safe.
Hello <%= '<b>'.html_safe + params[:name] + '</b>'.html_safe%>
For the following example, the input – params[:name] – will be a part of the string that is being converted to a SafeBuffer, therefore won’t be escaped later and as a result a Cross-Site Scripting vulnerability will be introduced.
Hello <%= "<b>#{params[:name]}</b>".html_safe%>
Transferring Data from Rails to HTML
HTML code consists of elements, attributes and values.
Passing Data to HTML Elements
The following style of data output is considered mostly safe for the sake of Rails’ built-in auto escaping, as discussed previously.
<b>Hello <%=params[:name]%></b>
When a malicious input hits this code location, Rail’s auto HTML escaping will take care of it before being sent back to the client. On this example, the data was sent within the <b> tag. While using nearly all of the HTML tags wouldn’t make difference there; a couple would have introduced security vulnerabilities. Some HTML tags change the context and open up another layer of parsing and execution. Those are: <script> and <style>. There should not be any untrusted data going there directly, except with very precise methods which will be discussed in the upcoming sections.
Passing Data to Attributes or Tag Names
HTML tag and attribute names should never be created from untrusted data, even after escaping or encoding, as those are parts of HTML code itself. By not doing so it would mean giving the ability to write HTML code to the outside parties.
Passing Data to Attribute Values
In HTML, there are some special attributes that either change execution context or have some special abilities, for example style, src and href attributes. The same in this case, untrusted data should not be directly passed inside the values for these.
Attributes that help define event handlers like onmouseover will be discussed in the JavaScript section. For other attributes, it can be considered safe with appropriate escaping. Other than that, there is only one caveat to be noted here, attribute values must always be placed within quotes. Here is an example:
<b name=”<%=params[:name]%>”>Hey All!</b>
Transferring Data from Rails to JavaScript
Modern web applications often need to pass data from the back-end to JavaScript. That is a perfectly valid need but should be done with precaution to avoid introducing any security vulnerabilities. Rails’ auto HTML escaping features will help us a lot to defend the situation here, but certainly it is not a silver bullet solution, and depending on the usage, there might be some details that need to be taken care of.
There are different encoding considerations that need to be made when transferring data from Rails to a different context than HTML, and if this is JavaScript, it can be quite tricky.
General Principles
First of all, we should know what we’re expecting to pass to JavaScript. It could be a string, an email address, an integer or something else. Depending on the case, we need to employ a different strategy.
Second, JavaScript can be considered as a sub-context under HTML, since it is living in an HTML document. Because of that, we should not forget to take care of HTML Injections with together JavaScript.
Third, as a general principle, there must never be any type of input, or untrusted data structure that is sent to eval or to related JavaScript functions, even after encoding or escaping. Since these functions are actually evaluating the incoming string, it is impossible to prevent code execution if any input is passed to:
- eval
- setInterval
- setTimeOut
- execScript
- write
- writeLn
- body.innerHtml
Alternatively, eval and related functions can be disabled by Content Security Policy restrictions.
Passing Numbers to JavaScript
A simple implementation of passing numbers to JavaScript can be something like:
<script>current_user_id = <%=param[:id]%></script>
If the above is attacked with a parameter such as ?id=alert(document.cookie) the output will be:
<script>current_user_id = alert(document.cookie)</script>
In this case, none of the characters are being escaped because there were not any HTML special character, yet it was possible to execute some simple JavaScript in the target web application’s browser context. This is happening because the user input is being put to JavaScript execution context without any precaution.
There is only one way to secure this design, we should make sure that we are passing just an integer, nothing else. This can be done a couple of ways, for example by employing whitelists. But for Ruby ecosystem, there is an easy way:
<script>current_user_id = <%=param[:id].to_i%></script>
Do not worry for the case of a string is sent to to_i function, it will basically cast everything to 0 if there are no valid integers.
Passing Text to JavaScript
JavaScript is a really interesting language with lots of special characters. Though it is really hard to transform all the special characters into harmless data, which is why the proper way to transfer text to JavaScript context is, only possible by locking up the data inside single or double quotes, and by making sure all the quotes and special HTML characters are properly escaped.
Here is an example:
<script>current_user_id = ‘<%=param[:id] %>’ </script>
In this particular case, the built-in Rails protection will be enough to help defend the situation. Since in order to inject JavaScript code, the malicious input should be going outside of the single quotes, which is not possible since all the single quotes and tag opening/closing characters are going to be escaped by ERB::Util#html_escape.
If we attack the above with the following parameter ?id=’+alert(1)+’ we get:
<script>current_user_id = ''+alert(1)+''</script>
As you can see, we could not get out of the string and therefore could not inject any code to execution context. If we re-iterate our thinking and decide to attack through getting out of <script> tag first, we can do this with ?id=”><script>alert(1)</script>:
<script>current_user_id = '"/><script>alert(1)</script>'</script>
But again, the attack was not successful since special HTML characters are being escaped.
Transferring JSON Data to HTML/JavaScript
What is JSON?
JSON is an open, standard data format that is used for transmitting data between different kind of systems. Utilizing JSON is really easy and convenient as many languages offer JSON creation and parsing routines. For more information on JSON refer to this Wikipedia entry.
This is a sample JSON object:
{"id":1,"name":”<great>”,"created_at":"2016-02-24T13:14:05.049Z","updated_at":"2016-02-24T13:14:05.049Z"}
As it is self-explanatory, JSON is built-on a collection of name/value pairs separated with a comma. Each name is followed by colon and a value. Value can be either a string in double quotes, number, true/false, null, another object or an array.
Creating JSON Objects in Ruby on Rails
Ruby on Rails developers can easily create JSON objects from Rails models and from some Ruby core classes by hitting to_json or as_json, such as (ref): Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable, Time, Date, DateTime, Process::Status. The proper way to generate JSON is using those functions. For example:
Loading development environment (Rails 4.2.5.1)
2.1.1 :001 > hash = {:name => 'jack', :age => "23"}
=> {:name=>"jack", :age=>"23"}
2.1.1 :002 > hash.to_json
=> "{\"name\":\"jack\",\"age\":\"23\"}"
For all of those classes, ActiveSupport’s JSON encoder – ActiveSupport::JSON.encode – is being used. We must stick to these functions and avoid trying to craft our own JSON strings. For example, the following implementation is exactly the type of thing we should stay away from:
@json_crafted = '{"name":"jack","id":”'+ params[:id] +'”}'
The downsides of this approach will be explained further down, alongside with examples.
What is JSON Escaping?
Rails uses ERB::Util#json_escape function to escape special JSON characters. This particular function does the substitution based on the following hash (ref).
{ '&' => '\u0026', '>' => '\u003e', '<' => '\u003c', "\u2028" => '\u2028', "\u2029" => '\u2029' }
Here is an example:
There is some JSON: <%= json_escape (User.last.to_json) %>
The response is:
There is some JSON: {"id":1,"name":"\u003cgreat\u003e","created_at":"2016-02-24T13:14:05.049Z","updated_at":"2016-02-24T13:14:05.049Z"}
As can seen in the generated JSON code, all the special characters are escaped. On Rails 4+, JSON strings generated through ActiveSupport’s JSON encoder do not require json_escape to be called since config.active_support.escape_html_entities_in_json set to true is on by default, and this will effectively escape all the HTML entities in the JSON values. Hence extra care should be taken manually if JSON.escape is used.
Even though with Rails 4 the default JSON escaping will be applied to the values, it is still a good idea to call ERB::Util#json_escape manually for JSON strings, as relying on auto protection might not be advisable for a few immediate reasons. First, such configuration is subject to change; second, to_json might have been overridden and third; the JSON data might not be created in the application but might from some other source.
For Rails versions before 4, ERB::Util#json_escape has a bug and as a result it does not return valid JSON objects. Old versions of Ruby on Rails are not getting new features, bug fixes and security fixes unless it is a severe one, as explained in Ruby on Rails maintenance policies. Therefore upgrading to Rails 4 is a good idea. But if you cannot at this time; you should either set config.active_support.escape_html_entities_in_json to true, backport patch, or override json_escape to fix the bug as explained in this example.
JSON to HTML
Passing JSON data to HTML is simple and should be fine as long as the JSON string is created in a meaningful way. When passing JSON data to HTML, it could be only placed in two locations; inside the HTML document and into an attribute. Both of those cases will be analyzed.
Passing Data to HTML Elements
Here is an example:
<b>Here is some json data:
<%={:name => "jack", :id => params[:id]}.to_json%>
</b>
The JSON object is created through Hash#to_json, which is the proper way as discussed before. Also, the JSON string is automatically encoded. So when attacked with ?id=<script>alert(1)</script> we get:
<b>Here is some json data: {"name":"jack","id":"\u003cscript\u003ealert(1)\u003c/script\u003e"}
</b>
The JSON string is also encoded by ERB::Util#html_escape. This is fine since we are just presenting this data through a web page, and looks fine since those HTML entities will be displayed as regular quotes on the screen. This was pretty straight forward, thanks to Rails!
Passing Data to Attribute Values
As it has been discussed before, style, src, href and other event handler attributes should be abstained from. Most likely use case like this will require the passing JSON object through an attribute, and parsing it in the JavaScript later. Example:
<b user-info="<%={:name => "jack", :id => params[:id]}.to_json%>"</b>
If the above is attacked with ?id=” onmouseover=alert(1) the below will be the result:
<b user-info="{"name":"jack","id":"\" onmouseover=alert(1)"}"</b>
Looks like all the quotes were being escaped and it was not possible to write some custom HTML attribute here. The onmouseover is treated just a string. Let’s see if those multiple layer of encoding broke the data in anyway.
> object = JSON.parse(document.getElementsByTagName("b")[0].getAttribute("user-info"))
< Object {name: "jack", id: "" onmouseover=alert(1)"}
> object.name
< "jack"
> object.id
< "" onmouseover=alert(1)"
Everything seems fine from JavaScript perspective as well. Still it is important not to send this user-controlled input to unsafe JavaScript dorks that call eval inside, and that are mentioned in the General Principles section.
JSON to JavaScript
Ruby Rails developers can transfer JSON formatted data from Rails to JavaScript. The following case is a good example. User’s name and id is sent to JavaScript through a JSON object.
<script>user = <%={:name => "jack", :id => params[:id]}.to_json%></script>
If the particular controller is being hit by ?id=34, the following HTML would be generated:
<script>user = {"name":"jack","id":34}</script>
As can be seen from the generated HTML, the JSON object is not valid because it has been escaped by Rails’ built-in protection, just as a regular string could have been. In order to create a valid JSON object, HTML escaping should be disabled.
<script>
user = <%={:name => "jack", :id => params[:id]}.to_json.html_safe%>
</script>
That fixes the problem of creating valid JSON, but opens up an XSS issue. So if attacked with ?id=/><script>alert(1)</script> the result will be:
<script>
user = {"name":"jack","email":"/><script>alert(1)</script>"}
</script>
Since HTML escaping was disabled, there is perfectly fine un-encoded quotation marks, but this also made JavaScript injection possible. We should escape values being carried in the JSON object. There is a function called ERB::Util#json_escape at our disposal to accomplish that.
<script>
user = <%=json_escape({:name => "jack", :id => params[:id]}.to_json.html_safe)%>
</script>
In the case if the same attack mentioned above is executed, the following HTML would be generated:
<script>
user = {"name":"jack","id":"\u003e\u003cscript\u003ealert(1)\u003c/script\u003e"}
</script>
This time a valid JSON object was created successfully without introducing any cross-site scripting issues. The special HTML characters were encoded in unicode to transform the harmless data.
Why JSON Creation Matters?
If a JSON object is not created in a proper way as explained above, the methodology defined above won’t work. Let’s examine the situation with an example:
@json_crafted = '{"name":"jack","id":”'+ params[:id] +'”}'
<script>
user=<%= json_escape(@json_crafted.html_safe) %>
</script>
The JSON object is manually crafted in the controller and passed to the JavaScript after json_escape. If the above is attacked with ?id=”+alert(1)+” the output will be:
<script>
user={"name":"jack","id":""+alert(1)+""}
</script>
In this case the web browser will execute alert(1) like any other JavaScript, and the return value from the alert function will be concatenated to the empty strings and assigned back to the id value. If you try to inspect the user variable in the JS console you will see:
>user
Object {name: “jack”, id: “undefined”}
This was possible because no mechanism is taking care of double quotes – which were used here to get out of JSON attribute value and execute JavaScript code. In other words, there was a JSON injection vulnerability.
Final Notes
Insecure Helpers
There are a bunch of insecure helper functions, such as content_tag, raw, link_to that can introduce XSS vulnerabilities if not used with care. We are not going to go through every single one here, but let’s talk about the one which is frequently used.
link_to
There is a small helper utility in ActionView::Helpers::UrlHelper called link_to that can be used to generate hyperlinks within an HTML document. Here is an example (ref):
link_to "Visit Other Site", "http://www.rubyonrails.org/"
# => <a href="http://www.rubyonrails.org/" >Visit Other Site</a>
This utility is fine as long as it is used with hardcoded data. But care should be taken if any untrusted input is linked this way as that utility could be used to do XSS attacks. Let’s change our view to demonstrate the idea.
link_to "Great link", params[:link]
When the above is attacked with ?id=javascript:alert(document.domain) the result is:
<a href="javascript:alert(document.domain)">Great link</a>
The JavaScript code embedded will work when the victim clicks the link. In order to fix this issue, the acceptable link target addresses should be restricted to only the following schemes: http, https. This can be easily done by URI#parse.
$ bundle exec rails c
Running via Spring preloader in process 39287
Loading development environment (Rails 4.2.5.1)
2.1.1 :001 > URI.parse("javascript:alert(1)")
=> #<URI::Generic:0x00000101bf1970 URL:javascript:alert(1)>
2.1.1 :002 > URI.parse("javascript:alert(1)").scheme
=> "javascript"
Content-Type Header
The Content-Type HTTP headers indicates the MIME type of the body of the request that is created, to help the user agent (in most cases the browser) correctly present the incoming data. According to the RFC2616 every HTTP/1.1 message that contains a body should include a Content-Type header (ref).
When serving resources, it is recommended to make sure that the correct Content-Type header is set to appropriately match the type of the resource being served. Otherwise as it is specified in the RFC, the user agent may try to guess it through MIME sniffing and that may lead to Cross-Site Scripting issues. See the section X-Content-Type-Options for details.
By default, Ruby on Rails will serve its requests with MIME content-type of text/html unless any of the following options are passed to the render method.
Render option |
Content-Type |
inline |
text/html |
json |
application/json |
plain |
text/plain |
js |
text/javascript |
xml |
application/xml |
raw |
text/html |
It is important to choose the correct option in order to serve data with the correct Content-Type header.
XSS Related to Security Headers
With Rails 4, every HTTP response is sent with the following headers by default (ref).
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
'X-XSS-Protection' => '1; mode=block',
'X-Content-Type-Options' => 'nosniff'
}
To prevent XSS vulnerabilities, both of the X-Content-Type-Options and X-XSS-Protection headers should be left with default settings.
X-Content-Type-Options
This is the header that can be configured to give a specific policy to the browser on MIME type preference. When this is set to nosniff, the browser doesn’t try to sniff the HTML document to guess its content type, instead it uses the one passed in the Content-Type header. MIME type sniffing is a standard functionality in browsers. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than that that it is intended to be.
The problem arises once a website allows users to upload content which is then published on the web server. If an attacker can carry out a XSS (Cross-site scripting) attack by manipulating the content in a way to be accepted by the web application and rendered as HTML by the browser, it is possible to inject code in for example an image file and make the victim execute it by viewing the image.
X-XSS-Protection
This particular header can be used to enable Cross-site scripting protection on the browser. This setting is recognized by Internet Explorer and Chrome.
HTTPOnly Cookies
HTTPOnly cookies cannot be read by client-side scripts, therefore marking a cookie as HTTPOnly can provide an additional layer of protection against Cross-site scripting attacks. On Ruby on Rails, cookies are not flagged as httponly by default, therefore should be set explicitly as shown in the below example:
cookies[:user_name] = {:value => "jack", :httponly => true}
Vulnerability Classification and Severity Table
Classification | ID / Severity |
---|---|
PCI v3.1 | 6.5.7 |
PCI v3.2 | 6.5.7 |
CAPEC | 19 |
CWE | 79 |
WASC | 8 |
OWASP 2013 | A3 |
HIPAA | 164.308(a) |
CVSS:3.0 |
CVSS:3.0/VA:N/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N
|
Netsparker | High |