summaryrefslogtreecommitdiffstats
path: root/dom/docs/scriptSecurity/xray_vision.rst
blob: 8a8a09320122d8f13379ffac8b06eaf12d4a6d16 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
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"