Xray vision

by 1 contributor:

Xray vision helps JavaScript running in a privileged security context safely access objects created by less privileged code, by showing the caller only the native version of the objects.

Gecko runs JavaScript from a variety of different sources and at a variety of different privilege levels.

  • The JavaScript code that, along with the C++ core, implements the browser itself is called chrome code and runs with system privileges. Extensions also run with chrome privileges. If chrome-privileged code is compromised, the attacker can take over the user's computer.
  • JavaScript loaded from normal web pages is called content code. Because this code is being loaded from arbitrary web pages it is regarded as untrusted and potentially hostile, both to other websites and to the user.
  • As well as these two levels of privilege, chrome code is able to create sandboxes. The security principal defined for the sandbox determines its privilege level. If an Expanded Principal is used, the sandbox is granted certain privileges over content code and is protected from direct access by content code. For example, the Add-on SDK runs content scripts inside sandboxes.

The security machinery in Gecko ensures that there's asymmetric access between code at different privilege levels: so for example, content code can't access objects created by chrome code, but chrome code can access objects created by content.

However, even the ability to access content objects can be a security risk for chrome code. JavaScript's a highly malleable language. Scripts running in web pages can add extra properties to DOM objects (also known as expando properties) and even redefine standard DOM objects to do something unexpected. If chrome code relies on such modified objects, it can be tricked into doing things it shouldn't.

For example: window.confirm() is a DOM API that's supposed to ask the user to confirm an action, and return a boolean depending on whether they clicked "OK" or "Cancel". A web page could redefine it to return true:

window.confirm = function() {
  return true;
}

Any privileged code calling this function and expecting its result to represent user confirmation would be deceived. This would be very naive, of course, but there are more subtle ways in which accessing content objects from chrome can cause security problems.

This is the problem that Xray vision is designed to solve. When a script accesses an object using Xray vision it sees only the native version of the object. Any expandos are invisible, and if any properties of the object have been redefined, it sees the original implementation, not the redefined version.

So in the example above, chrome code calling the content's window.confirm() would get the original version of confirm(), not the redefined version.

It's worth emphasising that even if content tricks chrome into running some unexpected code, that code does not run with chrome privileges. So this is not a straightforward privilege escalation attack, although it might lead to one if the chrome code is sufficiently confused.

How you get Xray vision

Privileged code automatically gets Xray vision whenever it accesses objects belonging to less-privileged code. So when chrome code accesses content objects, it sees them with Xray vision:

// chrome code
var transfer = gBrowser.contentWindow.confirm("Transfer all my money?");
// calls the native implementation

Note that using window.confirm() would be a terrible way to implement a security policy, and is only shown here to illustrate how Xray vision works.

When Add-on SDK content scripts or GreaseMonkey user scripts access the DOM, they get Xray vision as well, because they run in a sandbox at a higher privilege level than their content.

Waiving Xray vision

Xray vision is a kind of security heuristic, designed to make most common operations on untrusted objects simple and safe. However, there are some operations for which they are too restrictive: for example, if you need to see expandos on DOM objects. In cases like this you can waive Xray protection, but then you can no longer rely on any properties or functions being, or doing, what you expect. Any of them, even setters and getters, could have been redefined by untrusted code.

To waive Xray vision for an object you can use Components.utils.waiveXrays(object), or use the object's wrappedJSObject property:

// chrome code
var waivedWindow = Components.utils.waiveXrays(gBrowser.contentWindow);
var transfer = waivedWindow.confirm("Transfer all my money?");
// calls the redefined implementation
// chrome code
var waivedWindow = gBrowser.contentWindow.wrappedJSObject;
var transfer = waivedWindow.confirm("Transfer all my money?");
// calls the redefined implementation

In Add-on SDK content scripts and GreaseMonkey user scripts, you can use the global unsafeWindow as well: this is identical to window.wrappedJSObject.

Waivers are transitive: so if you waive Xray vision for an object, then you automatically waive it for all the object's properties. For example, window.wrappedJSObject.document gets you the waived version of document.

To undo the waiver again, call Components.utils.unwaiveXrays(waivedObject):

var unwaived = Components.utils.unwaiveXrays(waivedWindow);
unwaived.confirm("Transfer all my money?");
// calls the native implementation

Xrays for DOM objects

The primary use of Xray vision is for DOM objects: that is, the objects that represent the parts of a web page.

In Gecko, DOM objects have a dual representation: the canonical representation is in C++, and this is reflected into JavaScript for the benefit of JavaScript code. Any modifications to these objects, such as adding expandos or redefining standard properties, stays in the JavaScript reflection and does not affect the C++ representation.

The dual representation enables an elegant implementation of Xrays: the Xray just directly accesses the C++ representation of the original object, and doesn't go to the content's JavaScript reflection at all. Instead of filtering out modifications made by content, the Xray short-circuits the content completely.

This also makes the semantics of Xrays for DOM objects clear: they are the same as the DOM specification, since that is defined using the WebIDL, and the WebIDL also defines the C++ representation.

Xrays for JavaScript objects

Until recently, built-in JavaScript objects that are not part of the DOM, such as Date, Error, and Object, did not get Xray vision when accessed by more-privileged code.

Most of the time this is not a problem: the main concern Xrays solve is with untrusted web content manipulating objects, and web content is usually working with DOM objects. For example, if content code creates a new Date object, it will usually be created as a property of a DOM object, and then it will be filtered out by the DOM Xray:

// content code

// redefine Date.getFullYear()
Date.prototype.getFullYear = function() {return 1000};
var date = new Date();
// chrome code

// contentWindow is an Xray, and date is an expando on contentWindow
// so date is filtered out
gBrowser.contentWindow.date.getFullYear()
// -> TypeError: gBrowser.contentWindow.date is undefined

The chrome code will only even see date if it waives Xrays, and then, because waiving is transitive, it should expect to be vulnerable to redefinition:

// chrome code

Components.utils.waiveXrays(gBrowser.contentWindow).date.getFullYear();
// -> 1000

However, there are some situations in which privileged code will access JavaScript objects that are not themselves DOM objects and are not properties of DOM objects. For example:

  • the detail property of a CustomEvent fired by content could a JavaScript Object or Date as well as a string or a primitive
  • the return value of evalInSandbox() and any properties attached to the Sandbox object may be pure JavaScript objects

Also, the WebIDL specifications are starting to use JavaScript types such as Date and Promise: since WebIDL definition is the basis of DOM Xrays, not having Xrays for these JavaScript types starts to seem arbitrary.

So in Gecko 31 and 32 we've added Xray support for most JavaScript built-in objects.

Like DOM objects, most JavaScript built-in objects have underlying C++ state that is separate from their JavaScript representation, so the Xray implementation can go straight to the C++ state and guarantee that the object will behave as its specification defines:

// chrome code

var sandboxScript = 'Date.prototype.getFullYear = function() {return 1000};' +
                    'var date = new Date(); ';

var sandbox = Components.utils.Sandbox("https://example.org/");
Components.utils.evalInSandbox(sandboxScript, sandbox);

// Date objects are Xrayed
console.log(sandbox.date.getFullYear());
// -> 2014

// But you can waive Xray vision
console.log(Components.utils.waiveXrays(sandbox.date).getFullYear());
// -> 1000

To test out examples like this, you can use the Scratchpad in browser context for the code snippet, and the Browser Console to see the expected output.

Because code running in Scratchpad's browser context has chrome privileges, any time you use it to run code, you need to understand exactly what the code is doing. That includes the code samples in this article.

Xray semantics for Object and Array

The exceptions are Object and Array: their interesting state is in JavaScript, not C++. This means that the semantics of their Xrays have to be independently defined: they can't simply be defined as "the C++ representation".

The aim of Xray vision is to make most common operations simple and safe, avoiding the need to access the underlying object except in more involved cases. So the semantics defined for Object and Array Xrays aim to make it easy for privileged code to treat untrusted objects like simple dictionaries.

Any value properties of the object are visible in the Xray. If the object has properties which are themselves objects, and these objects are same-origin with the content, then their value properties are visible as well.

There are two main sorts of restrictions:

  • First, the chrome code might expect to rely on the prototype's integrity, so the object's prototype is protected:
    • the Xray has the standard Object or Array prototype, without any modifications that content may have done to that prototype. The Xray always inherits from this standard prototype, even if the underlying instance has a different prototype.
    • if a script has created a property on an object instance that shadows a property on the prototype, the shadowing property is not visible in the Xray
  • Second, we want to prevent the chrome code from running content code, so functions and accessor properties of the object are not visible in the Xray.

These rules are demonstrated in the script below, which evaluates a script in a sandbox, then examines the object attached to the sandbox.

To test out examples like this, you can use the Scratchpad in browser context for the code snippet, and the Browser Console to see the expected output.

Because code running in Scratchpad's browser context has chrome privileges, any time you use it to run code, you need to understand exactly what the code is doing. That includes the code samples in this article.

/*
The sandbox script:
* redefines Object.prototype.toSource()
* creates a Person() constructor that:
  * defines a value property "firstName" using assignment
  * defines a value property which shadows "constructor"
  * defines a value property "address" which is a simple object
  * defines a function fullName()
* using defineProperty, defines a value property on Person "lastName"
* using defineProperty, defines an accessor property on Person "middleName",
which has some unexpected accessor behavior
*/

var sandboxScript = 'Object.prototype.toSource = function() {'+
                    '  return "not what you expected?";' +
                    '};' +
                    'function Person() {' +
                    '  this.constructor = "not a constructor";' +
                    '  this.firstName = "Joe";' +
                    '  this.address = {"street" : "Main Street"};' +
                    '  this.fullName = function() {' +
                    '    return this.firstName + " " + this.lastName;'+
                    '  };' +
                    '};' +
                    'var me = new Person();' +
                    'Object.defineProperty(me, "lastName", {' +
                    '  enumerable: true,' +
                    '  configurable: true,' +
                    '  writable: true,' +
                    '  value: "Smith"' +
                    '});' +
                    'Object.defineProperty(me, "middleName", {' +
                    '  enumerable: true,' +
                    '  configurable: true,' +
                    '  get: function() { return "wait, is this really a getter?"; }' +
                    '});';

var sandbox = Components.utils.Sandbox("https://example.org/");
Components.utils.evalInSandbox(sandboxScript, sandbox);

// 1) trying to access properties in the prototype that have been redefined
// (non-own properties) will show the original 'native' version
// note that functions are not included in the output
console.log("1) Property redefined in the prototype:");
console.log(sandbox.me.toSource());
// -> "({firstName:"Joe", address:{street:"Main Street"}, lastName:"Smith"})"

// 2) trying to access properties on the object that shadow properties
// on the prototype will show the original 'native' version
console.log("2) Property that shadows the prototype:");
console.log(sandbox.me.constructor);
// -> function()

// 3) value properties defined by assignment to this are visible:
console.log("3) Value property defined by assignment to this:");
console.log(sandbox.me.firstName);
// -> "Joe"

// 4) value properties defined using defineProperty are visible:
console.log("4) Value property defined by defineProperty");
console.log(sandbox.me.lastName);
// -> "Smith"

// 5) accessor properties are not visible
console.log("5) Accessor property");
console.log(sandbox.me.middleName);
// -> undefined

// 6) accessing a value property of a value-property object is fine
console.log("6) Value property of a value-property object");
console.log(sandbox.me.address.street);
// -> "Main Street"

// 7) functions defined on the sandbox-defined object are not visible in the Xray
console.log("7) Call a function defined on the object");
try {
  console.log(sandbox.me.fullName());
}
catch (e) {
  console.error(e);
}
// -> TypeError: sandbox.me.fullName is not a function

// now with waived Xrays
console.log("Now with waived Xrays");

console.log("1) Property redefined in the prototype:");
console.log(Components.utils.waiveXrays(sandbox.me).toSource());
// -> "not what you expected?"

console.log("2) Property that shadows the prototype:");
console.log(Components.utils.waiveXrays(sandbox.me).constructor);
// -> "not a constructor"

console.log("3) Accessor property");
console.log(Components.utils.waiveXrays(sandbox.me).middleName);
// -> "wait, is this really a getter?"

console.log("4) Call a function defined on the object");
console.log(Components.utils.waiveXrays(sandbox.me).fullName());
// -> "Joe Smith"

Document Tags and Contributors

Contributors to this page: wbamberg
Last updated by: wbamberg,