mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 02:09:05 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			411 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			ReStructuredText
		
	
	
	
	
	
			
		
		
	
	
			411 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			ReStructuredText
		
	
	
	
	
	
Xray Vision
 | 
						|
===========
 | 
						|
 | 
						|
.. container:: summary
 | 
						|
 | 
						|
   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 using system
 | 
						|
   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 can 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.
 | 
						|
 | 
						|
| 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``:
 | 
						|
 | 
						|
.. code:: JavaScript
 | 
						|
 | 
						|
   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.
 | 
						|
 | 
						|
.. note::
 | 
						|
 | 
						|
   It's worth emphasizing 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:
 | 
						|
 | 
						|
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:
 | 
						|
 | 
						|
.. code:: JavaScript
 | 
						|
 | 
						|
   // chrome code
 | 
						|
   var transfer = gBrowser.contentWindow.confirm("Transfer all my money?");
 | 
						|
   // calls the native implementation
 | 
						|
 | 
						|
.. note::
 | 
						|
 | 
						|
   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.
 | 
						|
 | 
						|
.. _Waiving_Xray_vision:
 | 
						|
 | 
						|
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:
 | 
						|
 | 
						|
.. code:: JavaScript
 | 
						|
 | 
						|
   // chrome code
 | 
						|
   var waivedWindow = Components.utils.waiveXrays(gBrowser.contentWindow);
 | 
						|
   var transfer = waivedWindow.confirm("Transfer all my money?");
 | 
						|
   // calls the redefined implementation
 | 
						|
 | 
						|
.. code:: JavaScript
 | 
						|
 | 
						|
   // chrome code
 | 
						|
   var waivedWindow = gBrowser.contentWindow.wrappedJSObject;
 | 
						|
   var transfer = waivedWindow.confirm("Transfer all my money?");
 | 
						|
   // calls the redefined implementation
 | 
						|
 | 
						|
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):
 | 
						|
 | 
						|
.. code:: JavaScript
 | 
						|
 | 
						|
   var unwaived = Components.utils.unwaiveXrays(waivedWindow);
 | 
						|
   unwaived.confirm("Transfer all my money?");
 | 
						|
   // calls the native implementation
 | 
						|
 | 
						|
.. _Xrays_for_DOM_objects:
 | 
						|
 | 
						|
Xrays for DOM objects
 | 
						|
---------------------
 | 
						|
 | 
						|
The primary use of Xray vision is for DOM objects: that is, the
 | 
						|
objects that represent parts of the 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 <http://www.w3.org/TR/WebIDL/>`__, and the WebIDL also defines
 | 
						|
the C++ representation.
 | 
						|
 | 
						|
.. _Xrays_for_JavaScript_objects:
 | 
						|
 | 
						|
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:
 | 
						|
 | 
						|
.. code:: JavaScript
 | 
						|
 | 
						|
   // content code
 | 
						|
 | 
						|
   // redefine Date.getFullYear()
 | 
						|
   Date.prototype.getFullYear = function() {return 1000};
 | 
						|
   var date = new Date();
 | 
						|
 | 
						|
.. code:: JavaScript
 | 
						|
 | 
						|
   // 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:
 | 
						|
 | 
						|
.. code:: JavaScript
 | 
						|
 | 
						|
   // 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 be 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 an 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:
 | 
						|
 | 
						|
.. code:: JavaScript
 | 
						|
 | 
						|
   // 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
 | 
						|
 | 
						|
.. note::
 | 
						|
 | 
						|
   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:
 | 
						|
 | 
						|
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.
 | 
						|
 | 
						|
.. note::
 | 
						|
 | 
						|
   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.
 | 
						|
 | 
						|
.. code:: JavaScript
 | 
						|
 | 
						|
   /*
 | 
						|
   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"
 |