Xray Vision

Xray Vision 帮助在特权安全上下文中运行的 JavaScript 安全地访问由权限较低的代码创建的对象,方法是仅向调用方显示对象的原生版本。

Gecko 从各种不同的来源和各种不同的权限级别运行 JavaScript。

  • 与 C++ 核心一起实现浏览器本身的 JavaScript 代码称为chrome 代码,并使用系统权限运行。如果 chrome 特权代码遭到破坏,攻击者可以接管用户的计算机。

  • 从普通网页加载的 JavaScript 代码称为内容代码。由于此代码是从任意网页加载的,因此被视为不受信任且可能具有敌意的,既对其他网站也对用户构成威胁。

  • 除了这两个权限级别之外,chrome 代码还可以创建沙箱。为沙箱定义的安全主体决定其权限级别。如果使用扩展主体,则沙箱将被授予对内容代码的某些权限,并受到内容代码直接访问的保护。

Gecko 中的安全机制确保不同权限级别的代码之间存在非对称访问:例如,内容代码无法访问 chrome 代码创建的对象,但 chrome 代码可以访问内容创建的对象。
但是,即使是访问内容对象的能力也可能对 chrome 代码构成安全风险。JavaScript 是一种高度可塑的语言。在网页中运行的脚本可以向 DOM 对象添加额外的属性(也称为扩展属性),甚至可以重新定义标准 DOM 对象以执行意外的操作。如果 chrome 代码依赖于此类修改后的对象,则可能会被诱骗执行不应该执行的操作。
例如:window.confirm() 是一个 DOM API,它应该要求用户确认操作,并根据他们单击“确定”或“取消”返回布尔值。网页可以将其重新定义为返回 true
window.confirm = function() {
  return true;
}

任何调用此函数并期望其结果表示用户确认的特权代码都会被欺骗。当然,这将非常天真,但还有更多微妙的方式,从 chrome 访问内容对象会导致安全问题。

这就是 Xray Vision 旨在解决的问题。当脚本使用 Xray Vision 访问对象时,它只会看到对象的原生版本。任何扩展属性都是不可见的,如果对象的任何属性已被重新定义,它会看到原始实现,而不是重新定义的版本。
因此,在上面的示例中,调用内容的 window.confirm() 的 chrome 代码将获得 confirm() 的原始版本,而不是重新定义的版本。

注意

值得强调的是,即使内容欺骗 chrome 运行一些意外的代码,该代码也不会以 chrome 权限运行。因此,这不是简单的权限提升攻击,尽管如果 chrome 代码足够混乱,它可能会导致权限提升攻击。

如何获得 Xray Vision

特权代码在访问属于权限较低代码的对象时会自动获得 Xray Vision。因此,当 chrome 代码访问内容对象时,它会使用 Xray Vision 查看这些对象。

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

注意

请注意,使用 window.confirm() 将是实现安全策略的糟糕方法,此处仅显示其工作原理。

放弃 Xray Vision

Xray Vision 是一种安全启发式方法,旨在使对不受信任对象的多数常见操作变得简单和安全。但是,对于某些操作,它们限制性太强:例如,如果您需要查看 DOM 对象上的扩展属性。在这种情况下,您可以放弃 Xray 保护,但随后您不能再依赖任何属性或函数是或执行您期望的操作。任何一个,即使是设置器和获取器,都可能已被不受信任的代码重新定义。
要放弃对象的 Xray Vision,您可以使用 Components.utils.waiveXrays(object),或使用对象的 wrappedJSObject 属性。
// 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

放弃是可传递的:因此,如果您放弃对象的 Xray Vision,那么您会自动放弃其所有属性的 Xray Vision。例如,window.wrappedJSObject.document 将为您提供 document 的放弃版本。

要撤消放弃,请调用 Components.utils.unwaiveXrays(waivedObject)。

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

DOM 对象的 Xray

Xray Vision 的主要用途是用于 DOM 对象:即表示网页部分的对象。

在 Gecko 中,DOM 对象具有双重表示:规范表示在 C++ 中,这会反映到 JavaScript 中,以供 JavaScript 代码使用。对这些对象的任何修改,例如添加扩展属性或重新定义标准属性,都将保留在 JavaScript 反射中,并且不会影响 C++ 表示。

双重表示使 Xray 的实现变得优雅:Xray 直接访问原始对象的 C++ 表示,并且根本不会转到内容的 JavaScript 反射。Xray 不会过滤掉内容所做的修改,而是完全绕过了内容。

这也使 DOM 对象的 Xray 语义变得清晰:它们与 DOM 规范相同,因为 DOM 规范是使用 WebIDL 定义的,并且 WebIDL 也定义了 C++ 表示。

JavaScript 对象的 Xray

直到最近,不是 DOM 部分的内置 JavaScript 对象,例如 DateErrorObject,在被权限更高的代码访问时不会获得 Xray Vision。

大多数情况下,这不是问题:Xray 解决的主要问题是不受信任的 Web 内容操纵对象,而 Web 内容通常使用 DOM 对象。例如,如果内容代码创建一个新的 Date 对象,它通常会作为 DOM 对象的属性创建,然后它将被 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

chrome 代码只有在放弃 Xray 时才会看到 date,并且由于放弃是可传递的,因此它应该预期容易受到重新定义的影响。

// chrome code

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

但是,在某些情况下,特权代码将访问本身不是 DOM 对象且不是 DOM 对象属性的 JavaScript 对象。例如

  • 内容触发的 CustomEvent 的 detail 属性可以是 JavaScript 对象或 Date,也可以是字符串或基本类型。

  • evalInSandbox() 的返回值以及附加到 Sandbox 对象的任何属性都可能是纯 JavaScript 对象。

此外,WebIDL 规范开始使用 JavaScript 类型,例如 DatePromise:由于 WebIDL 定义是 DOM Xray 的基础,因此对于这些 JavaScript 类型没有 Xray 开始显得随意。

因此,在 Gecko 31 和 32 中,我们为大多数 JavaScript 内置对象添加了 Xray 支持。

与 DOM 对象一样,大多数 JavaScript 内置对象都有一个底层的 C++ 状态,该状态与其 JavaScript 表示是分开的,因此 Xray 实现可以直接转到 C++ 状态并保证对象的行为与其规范定义一致。

// 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

注意

要测试此类示例,您可以使用浏览器上下文中的 Scratchpad 运行代码片段,并使用浏览器控制台查看预期的输出。

由于在 Scratchpad 的浏览器上下文中运行的代码具有 chrome 权限,因此每次使用它运行代码时,都需要准确了解代码的作用。这包括本文中的代码示例。

Object 和 Array 的 Xray 语义

例外情况是 ObjectArray:它们有趣的状态在 JavaScript 中,而不是在 C++ 中。这意味着必须独立定义其 Xray 的语义:它们不能简单地定义为“C++ 表示”。

Xray Vision 的目标是使多数常见操作变得简单和安全,避免在更复杂的情况下除了访问底层对象之外的任何操作。因此,为 ObjectArray Xray 定义的语义旨在使特权代码能够轻松地将不受信任的对象视为简单的字典。

对象的任何值属性在 Xray 中都是可见的。如果对象具有本身也是对象的属性,并且这些对象与内容具有相同的来源,则其值属性也将可见。

主要有两种限制

  • 首先,chrome 代码可能期望依赖于原型的完整性,因此对象的原型受到保护。

    • Xray 具有标准的 ObjectArray 原型,没有任何内容可能对该原型进行的修改。即使底层实例具有不同的原型,Xray 始终继承自此标准原型。

    • 如果脚本在对象实例上创建了一个属性,该属性会覆盖原型上的属性,则该覆盖属性在 Xray 中不可见。

  • 其次,我们希望防止 chrome 代码运行内容代码,因此对象的函数和访问器属性在 Xray 中不可见。

以下脚本演示了这些规则,该脚本在沙箱中评估脚本,然后检查附加到沙箱的对象。

注意

要测试此类示例,您可以使用浏览器上下文中的 Scratchpad 运行代码片段,并使用浏览器控制台查看预期的输出。

由于在 Scratchpad 的浏览器上下文中运行的代码具有 chrome 权限,因此每次使用它运行代码时,都需要准确了解代码的作用。这包括本文中的代码示例。

/*
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"