Friday, April 19, 2013

"jQuery Migrate" is a Sink, too?!

or How "jQuery Migrate" un-fixes a nasty DOMXSS without telling us..

Foreword

Today Mario Heiderich of Cure53 tweeted the following message:

"@0x6D6172696F Does anyone know why jquery.com has a special jQuery 1.9.1 version that is still vulnerable to $(location.hash)?"

What happened after that message might be considered to be the discovery of a rather interesting bug - which Mario and me will try to wrap up in this joint blog-post.

In short words:
an official jQuery plugin un-fixes a long-gone DOMXSS bug and brings it back - even on the jQuery homepage itself!

The Long Story

First, let's give the word to Mario and hear what he has to say:

"Some days ago, while being engaged in a workshop, I discovered a funny thing.
I visited the jQuery.com homepage and tried playing around with the DOMXSS jQuery came shipped with some months ago.
The one, that could be exploited by allowing arbitrary user input to the $() function - remember?
My goal was to demonstrate the attendees, how the XSS worked, why it worked and how it was fixed.

So, I wanted to test if there's still ways to activate it and get past the regular expression they installed to mitigate the bug. First thing was to try the obvious, set location.hash to <svg onload="alert(1)"> and execute $(location.hash) via the Firebug console expecting it NOT to work.
But.
It worked.
Admittedly confused, I checked the jQuery version they deployed and it was the latest - jQuery 1.9.1.
That couldn't be - I must have made something wrong!

I decided to build a small dynamic checker to see which exact version of jQuery is vulnerable to this bug, and which is not. The tool I created - really ugly code, be warned - is here.
It simply fetches all the jQuery versions from 1.3.0 up to 2.0.0 from the Google API, tests the vector via location.hash and then goes red or stays green depending on whether the code executed or not. As can clearly be seen:
jQuery 1.6.2 was the last vulnerable version. And 1.9.1 is not!



So, what did the jQuery people do? Why is their latest version vulnerable but the actual version isn't? How did they get their version to be buggy again? In my desperate helplessness I tweeted and Stefano replied. He found the cause for this quirky behaviour of eternal madness."

Now, Stefano (me:) had a deep look into the code and started the engine of... well, you know it, DOMinatorPro to see what kind of roguery is going on here.

"Since the day I started to develop DOMinatorPro and write DOMXSSWiki I thoroughly studied jQuery internals in its very own, so when Mario tweeted about that, I immediately thought about some weird misunderstood mixed jQuery version behaviour, as sometimes have happened to me.
Yet, here's the screen-shot:




..and the version info indeed clearly says: 1.9.1!!!
You can easily check that via $.expando by the way - no need to dig in the site sources or script code itself.
But, how's that possible?
Can DOMinatorPro help  me?
Let's see. By simply doing the same with DOMinatorPro, it will show the following alert:


The StackTrace says:

buildFragment(elems="#<s>sss", context="[object HTMLDocument]",scripts="undefined", selection="undefined")jquery-1.9.1.js (line 6481)

parseHTML
(data="#<s>sss", context="[object HTMLDocument]",keepScripts="true")jquery-1.9.1.js (line 521)

init(selector="#<s>sss", context="undefined", rootjQuery="[object Object]")jquery....1.0.js (line 213)

jQuery(selector="#<s>sss", context="undefined")jquery-1.9.1.js (line 48)


ParseHTML? No way! What are those files? Wait a moment, that jquery....1.0.js (line 213) is actually:
http://code.jquery.com/jquery-migrate-1.1.0.js !!
Let's see what's there:

var matched, browser,
 oldInit = jQuery.fn.init,
 oldParseJSON = jQuery.parseJSON,
 // Note this does NOT include the #9521 XSS fix from 1.7!
 rquickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*|#([\w\-]*))$/;

// $(html) "looks like html" rule change
jQuery.fn.init = function( selector, context, rootjQuery ) {
 var match;

 if ( selector && typeof selector === "string" && !jQuery.isPlainObject( context ) &&
   (match = rquickExpr.exec( selector )) && match[1] ) {
  // This is an HTML string according to the "old" rules; is it still?
  if ( selector.charAt( 0 ) !== "<" ) {
   migrateWarn("$(html) HTML strings must start with '<' character");
  }
  // Now process using loose rules; let pre-1.8 play too
  if ( context && context.context ) {
   // jQuery object as context; parseHTML expects a DOM object
   context = context.context;
  }
  if ( jQuery.parseHTML ) {
         return oldInit.call( this, jQuery.parseHTML( jQuery.trim(selector), context, true ),
 /* Vuln Call Here !! -^*/  context, rootjQuery );
  }
 }
 return oldInit.apply( this, arguments );
};
jQuery.fn.init.prototype = jQuery.fn; 

So, by looking at the oldInit.call what happens here is that another jQuery sink is called (!) and that's jQuery.parseHTML."

Why is that?

It's probably a way to give back compatibility by recreating the old behavior using parseHTML, but jQuery guys forgot that there are two fixes that try to patch a wrong design choice, and the first one comes from Version > 1.6.2. These include the dreaded DOMXSS from Mala.
 
Can it be worked around? 

Mario wrote this quick and - as he calls it himself - incredibly dirty fix, which will address the unexpected abuse of this classical pattern:

function jQMfix(){      if(/</.test(location.hash)) {           location.hash=location.hash.replace(/</g,'')      } }
onhashchange=jQMfix; jQMfix();

The  fix is - and that is Mario speaking again - extremely dirty and everything but production ready.
But it shows a way to make sure the location.hash cannot be abused to cause DOMXSS on your precious website.
If your JavaScript is.. "special" it can of course still happen.

But you get the gist - make sure that if you use the jQuery Migration plugin that your web-app stays safe from DOMXSS.

Here's yet another way to approach it, from Stefano, but keep in mind that it's completely untested and more an example on how to approach it:

jQuery = $ =( function (r) {return function jQuery(a,b){
  if(a.charAt(0)==='#') 
    a=a.replace(/</g,'')
  r.apply(null,[a,b])
 }
} )(jQuery)
..and, of course, feel free to improve it and paste it in a comment.

We do see a lot of plugin-caused DOMXSS in the wild - but a plugin that purposely un-fixes an existing and well-known XSS bug? That's something :-)

Also, greetings to the fellow attendees of the jQuery UK conference that is happening right now.

Have a beautiful (0-)day! :)