I ran into an interesting issue yesterday related to the use of jQuery and a potential XSS (cross-site scripting) vulnerability. It was an easy mistake to make, and one I unfortunately see (and occasionally make myself) all too often.
So let’s break things down and prevent this bug from coming up again!
The Attack
A casual reader reported the attack. It seems they could append some seemingly arbitrary text to a visible URL and cause the page to trigger an alert. Digging deeper, they also pointed out that a similar attack could redirect the page to any URL they wanted.
For example, [cci]https://eamann.com/#”>[/cci] would cause the page to post an alert of “test.”[ref]Note: I’m using my domain in these examples, but this site was not affected by this vulnerability.[/ref] Similarly, [cci]https://eamann.com/#”>[/cci] would force the browser to automatically redirect to Facebook.
On the surface, this seems like a non-issue – an attacker could display an alert or force a page to redirect.[ref]Typically, redirections are a negative thing. But if you’re clicking on a link like this in the first place, you should be aware that something is up.[/ref]
The attack itself, though, goes much deeper and has much more nefarious implications. Essentially, this vulnerability allows an attacker to execute any arbitrary JavaScript they want, from the user’s own context. An alert or redirect to Facebook is trivial – POSTing the current user’s cookies to a 3rd-party site, though, is not.
If a site is vulnerable to this attack, I could craft a URL that would grab the contents of the page’s cookies and send them to anyone I desire. Once I have your cookies, I can spoof your logged-in session even if you logged in via HTTPS.
Doesn’t seem like a non-issue any more, does it?
The Cause
It turns out, a 4-line script on the page was at fault. Some fancy tab switching going on in an internal page required JavaScript to read the current URL hash (the component after the # sign) and use it in an element selector to help the page know which tab to load. Like many developers, jQuery was used to build a quick proof-of-concept of the feature – and ended up being shipped to production in finished code.
The prototype looked something like: [cci]if ( $( ‘.class .’ + window.location.hash + ‘ ul’ ) ) { …[/cci]
On the surface, this looks just fine. Until, that is, you remember what jQuery does behind the scenes with selectors. First, jQuery will attempt to parse the selector as a selector – the intended use case. If the selector fails to validate, jQuery assumes the string passed in is instead a block of HTML, and subsequently attempts to parse it.
[cci].class .”> ul[/cci] will force jQuery to attempt to create an image tag, with a broken source attribute, and an error handler containing the attacker’s desired script package. Since the source is broken (in this case just an “M”), the error handler triggers immediately and executes whatever script the attacker wants.
It could display an alert. It could redirect the page. It could grab the browser’s cookies (including your authenticated session cookie) and send them to a remote party.
It’s a pretty significant bug, and was created merely because someone failed to recognize the security implications of a quick proof-of-concept script and shipped it to production as-is.
The Solution
Instead of jQuery, the selector should be parsed using native DOM methods. [cci]document.querySelectorAll()[/cci] serves the same purpose here. Unlike jQuery, it throws a syntax exception if you attempt to pass the broken image tag used in the attack – an exception that is easily caught and discarded.
[cci]if ( 0 < document.querySelectorAll('.class .' + window.location.hash + ' ul' ).length ) { ...[/cci] serves the same purpose in this conditional. Wrapped in a try/catch block, it completely plugs the hole and keeps the site's naive selector logic from opening a door to an attacker. The original site hosting the vulnerable code has been patched, and everyone involved learned a helpful lesson[ref]I never fault anyone for making a mistake like this. After seeing this bug, I went back through my own code and found numerous examples that might present similar vulnerabilities. Coding is a collaborative and iterative process, so the fact that someone caught the bug and that it was fixed quickly speaks volumes about the success of the development team involved. I measure success not in the amount of bug-free code produced, but in a team’s overall ability to learn from and correct their mistakes.[/ref] about their code: never trust any form of user input, even if it’s coming from an allegedly trustworthy source. Referencing properties on the global [cci]window[/cci] object feels safe because it’s not coming directly from the user; just always keep in mind where the values in those properties come from.
The recent discovery of the bash-related Shellshock bug reminds us just how important it is to always identify the source of parameters and, even if they’re a trusted source, to be skeptical of what data is passed in regardless.