Friday, July 26, 2019

Error.prepareStackTrace allows to catch cross-origin script errors


Normally it is not possible to properly catch JavaScript runtime errors, which are triggered by cross-domain JavaScript files via HTML script tags. I discovered that prepareStackTrace can bypass this restriction, when the developer console is opened by the user (I did not discover a bypass for the need of the console yet). This can lead to a cross-domain information leak as shown later. Maybe you, the reader, can find more use cases.

Error.prepareStackTrace


I discovered it via the following tweet: https://twitter.com/intenttoship/status/1146097840118272000 > "Blink: Intent to Remove: 'getThis' and 'getFunction' from the CallSite API"

This lead me to: https://v8.dev/docs/stack-trace-api#customizing-stack-traces

The JavaScript V8 engine exposes Error.prepareStackTrace(error, structuredStackTrace), which allows to capture error stracktraces.
The error parameter contains the error description. The structured stack trace is an array of CallSite objects, each of which represents a stack frame. A CallSite object defines the following methods:
- getThis: returns the value of this
- getTypeName: returns the type of this as a string. This is the name of the function stored in the constructor field of this, if available, otherwise the object’s [[Class]] internal property.
- getFunction: returns the current function
- getFunctionName: returns the name of the current function, typically its name property. If a name property is not available an attempt is made to infer a name from the function’s context.
- getMethodName: returns the name of the property of this or one of its prototypes that holds the current function
- getFileName: if this function was defined in a script returns the name of the script
[...]

Lets look at an example:

<!doctype HTML>
<meta charset="UTF-8" />
<script>
Error.prepareStackTrace = function(error,stacks)
{
   alert(error);
   alert("FunctionName: " + stacks[0].getFunctionName())
   return "MyStackObject";
}
function test(){
try{
  nothere();
  }
  catch(e){
  e.stack
  }

}
test()
</script>


This will alert the following text: "ReferenceError: nothere is not defined" and "FunctionName: test". It can be seen that the error parameter contains the string representation thrown by the JavaScript engine, as the function is not defined. The first stack value is the test function as it triggered the runtime error and therefore its name is returned by the getFunctionName call.

In case you would include remote script via <script src="http://remote/remotejs">, which causes an undefined JavaScript error, prepareStackTrace would not catch this error and it would only be displayed in the developer console.

The developer console

One interesting behavior I discovered is the fact, that Error.prepareStackTrace catches more errors when the developer console is opened. Suddenly it is able to catch "ReferenceError: <> is not defined" errors triggered by remotely loaded JavaScript files.
This does not sound too interesting at first. But modern websites often have JSON resources, which return information in an array like syntax: ["john","doe","address"], which can be loaded via a HTML script tag.
Gareth Heyes explored these kind of resources and documented his attack vectors back in 2016. One vector - Stealing JSON feeds in Chrome - abused the script tags charset attribute. By specifying the charset UTF-16BE, the remotely loaded JSON array will be interpreted as an UTF-16 variable. As it is not defined, it will cause a JavaScript runtime error. As usual Gareth abused JavaScript itself to be able to catch this undefined error. After catching the undefined UTF-16 variable, it is necessary to do some bit shifting to convert it back to the original ASCII text, therefore leaking the information to another domain. His way of catching the error was fixed in Google Chrome. 

But Error.prepareStrackTrace allows to catch "remote" undefined errors as well as soon as the developer console is opened. Therefore it is possible to abuse this attack again

The PoC


The following PoC needs to be hosted on a HTTP (not HTTPS) website or otherwise a mixed-content warning will be displayed. Of course you need to use a browser, which is using the JavaScript V8 engine like Chrome. It will display 'ReferenceError: 嬢獵灥牳散牥琢Ⱒ慢挢 is not defined', as it does not decode the UTF-16 variable:

<!doctype HTML>
<meta charset="UTF-8" />
<script>
Error.prepareStackTrace = function(error,stacks)
{
 alert(error);
 stacks[0].getThis().alert(stacks[0].getThis().location);
        return "MyStackObject";
}
</script>
<script charset="UTF-16BE"
src="http://subdomain1.portswigger-labs.net/utf-16be/chrome_steal_json_data_with_proxy/array.php">
</script>
<body>
<h1> open the developer console </h1>


The standard:


The behavior of Error.prepareStackTrace violates the standard (in case the developer console is opened):

6. If script's muted errors is true, then set message to "Script error.", urlString to the empty string, line and col to 0, and errorValue to null.

A muted errors boolean:

A boolean which, if true, means that error information will not be provided for errors in this script. This is used to mute errors for cross-origin scripts, since that can leak private information.