Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This module handles wrapping privileged objects so that they can be exposed
* to unprivileged contexts. It is only to be used in automated tests.
*
* Its exact semantics are also liable to change at any time, so any callers
* relying on undocumented behavior or subtle platform features should expect
* breakage. Those callers should, wherever possible, migrate to fully
* chrome-privileged scripts when they need to interact with privileged APIs.
*/
// XPCNativeWrapper is not defined globally in ESLint as it may be going away.
// See bug 1481337.
/* globals XPCNativeWrapper */
Cu.crashIfNotInAutomation();
let wrappedObjects = new WeakMap();
let perWindowInfo = new WeakMap();
let noAutoWrap = new WeakSet();
function isWrappable(x) {
if (typeof x === "object") {
return x !== null;
}
return typeof x === "function";
}
function isWrapper(x) {
try {
return isWrappable(x) && wrappedObjects.has(x);
} catch (e) {
// If `x` is a remote object proxy, trying to access an unexpected property
// on it will throw a security error, even though we're chrome privileged.
// However, remote proxies are not SpecialPowers wrappers, so:
return false;
}
}
function unwrapIfWrapped(x) {
return isWrapper(x) ? unwrapPrivileged(x) : x;
}
function wrapIfUnwrapped(x, w) {
return isWrapper(x) ? x : wrapPrivileged(x, w);
}
function isObjectOrArray(obj) {
if (Object(obj) !== obj) {
return false;
}
let arrayClasses = [
"Object",
"Array",
"Int8Array",
"Uint8Array",
"Int16Array",
"Uint16Array",
"Int32Array",
"Uint32Array",
"Float32Array",
"Float64Array",
"Uint8ClampedArray",
];
let className = Cu.getClassName(obj, true);
return arrayClasses.includes(className);
}
// In general, we want Xray wrappers for content DOM objects, because waiving
// Xray gives us Xray waiver wrappers that clamp the principal when we cross
// compartment boundaries. However, there are some exceptions where we want
// to use a waiver:
//
// * Xray adds some gunk to toString(), which has the potential to confuse
// consumers that aren't expecting Xray wrappers. Since toString() is a
// non-privileged method that returns only strings, we can just waive Xray
// for that case.
//
// * We implement Xrays to pure JS [[Object]] and [[Array]] instances that
// filter out tricky things like callables. This is the right thing for
// security in general, but tends to break tests that try to pass object
// literals into SpecialPowers. So we waive [[Object]] and [[Array]]
// instances before inspecting properties.
//
// * When we don't have meaningful Xray semantics, we create an Opaque
// XrayWrapper for security reasons. For test code, we generally want to see
// through that sort of thing.
function waiveXraysIfAppropriate(obj, propName) {
if (
propName == "toString" ||
isObjectOrArray(obj) ||
/Opaque/.test(Object.prototype.toString.call(obj))
) {
return XPCNativeWrapper.unwrap(obj);
}
return obj;
}
// We can't call apply() directy on Xray-wrapped functions, so we have to be
// clever.
function doApply(fun, invocant, args) {
// We implement Xrays to pure JS [[Object]] instances that filter out tricky
// things like callables. This is the right thing for security in general,
// but tends to break tests that try to pass object literals into
// SpecialPowers. So we waive [[Object]] instances when they're passed to a
// SpecialPowers-wrapped callable.
//
// Note that the transitive nature of Xray waivers means that any property
// pulled off such an object will also be waived, and so we'll get principal
// clamping for Xrayed DOM objects reached from literals, so passing things
// like {l : xoWin.location} won't work. Hopefully the rabbit hole doesn't
// go that deep.
args = args.map(x => (isObjectOrArray(x) ? Cu.waiveXrays(x) : x));
return Reflect.apply(fun, invocant, args);
}
function wrapPrivileged(obj, win) {
// Primitives pass straight through.
if (!isWrappable(obj)) {
return obj;
}
// No double wrapping.
if (isWrapper(obj)) {
throw new Error("Trying to double-wrap object!");
}
let { windowID, proxies, handler } = perWindowInfo.get(win) || {};
// |windowUtils| is undefined if |win| is a non-window object
// such as a sandbox.
let currentID = win.windowGlobalChild
? win.windowGlobalChild.innerWindowId
: 0;
// Values are dead objects if the inner window is changed.
if (windowID !== currentID) {
windowID = currentID;
proxies = new WeakMap();
handler = Cu.cloneInto(SpecialPowersHandler, win, {
cloneFunctions: true,
});
handler.wrapped = new win.WeakMap();
perWindowInfo.set(win, { windowID, proxies, handler });
}
if (proxies.has(obj)) {
return proxies.get(obj).proxy;
}
let className = Cu.getClassName(obj, true);
if (className === "ArrayBuffer") {
// Since |new Uint8Array(<proxy>)| doesn't work as expected, we have to
// return a real ArrayBuffer.
return obj instanceof win.ArrayBuffer ? obj : Cu.cloneInto(obj, win);
}
let dummy;
if (typeof obj === "function") {
dummy = Cu.exportFunction(function () {}, win);
} else {
dummy = new win.Object();
}
handler.wrapped.set(dummy, { obj });
let proxy = new win.Proxy(dummy, handler);
wrappedObjects.set(proxy, obj);
switch (className) {
case "AnonymousContent":
// Caching anonymous content will cause crashes (bug 1636015).
break;
case "CSS2Properties":
case "CSSStyleRule":
case "CSSStyleSheet":
// Caching these classes will cause memory leaks.
break;
default:
proxies.set(obj, { proxy });
break;
}
return proxy;
}
function unwrapPrivileged(x) {
// We don't wrap primitives, so sometimes we have a primitive where we'd
// expect to have a wrapper. The proxy pretends to be the type that it's
// emulating, so we can just as easily check isWrappable() on a proxy as
// we can on an unwrapped object.
if (!isWrappable(x)) {
return x;
}
// If we have a wrappable type, make sure it's wrapped.
if (!isWrapper(x)) {
throw new Error("Trying to unwrap a non-wrapped object!");
}
// unwrapped.
return wrappedObjects.get(x);
}
function wrapExceptions(global, fn) {
try {
return fn();
} catch (e) {
throw wrapIfUnwrapped(e, global);
}
}
let SpecialPowersHandler = {
construct(target, args) {
// The arguments may or may not be wrappers. Unwrap them if necessary.
var unwrappedArgs = Array.from(Cu.waiveXrays(args), x =>
unwrapIfWrapped(Cu.unwaiveXrays(x))
);
// We want to invoke "obj" as a constructor, but using unwrappedArgs as
// the arguments.
let global = Cu.getGlobalForObject(this);
return wrapExceptions(global, () =>
wrapIfUnwrapped(
Reflect.construct(this.wrapped.get(target).obj, unwrappedArgs),
global
)
);
},
apply(target, thisValue, args) {
let wrappedObject = this.wrapped.get(target).obj;
let global = Cu.getGlobalForObject(this);
// The invocant and arguments may or may not be wrappers. Unwrap
// them if necessary.
var invocant = unwrapIfWrapped(thisValue);
return wrapExceptions(global, () => {
if (noAutoWrap.has(wrappedObject)) {
args = Array.from(Cu.waiveXrays(args), x => Cu.unwaiveXrays(x));
return doApply(wrappedObject, invocant, args);
}
if (wrappedObject.name == "then") {
args = Array.from(Cu.waiveXrays(args), x =>
wrapCallback(Cu.unwaiveXrays(x), global)
);
} else {
args = Array.from(Cu.waiveXrays(args), x =>
unwrapIfWrapped(Cu.unwaiveXrays(x))
);
}
return wrapIfUnwrapped(doApply(wrappedObject, invocant, args), global);
});
},
has(target, prop) {
return Reflect.has(this.wrapped.get(target).obj, prop);
},
get(target, prop) {
let global = Cu.getGlobalForObject(this);
return wrapExceptions(global, () => {
let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop);
let val = Reflect.get(obj, prop);
return wrapIfUnwrapped(val, global);
});
},
set(target, prop, val) {
return wrapExceptions(Cu.getGlobalForObject(this), () => {
let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop);
return Reflect.set(obj, prop, unwrapIfWrapped(val));
});
},
delete(target, prop) {
return wrapExceptions(Cu.getGlobalForObject(this), () => {
return Reflect.deleteProperty(this.wrapped.get(target).obj, prop);
});
},
defineProperty() {
throw new Error(
"Can't call defineProperty on SpecialPowers wrapped object"
);
},
getOwnPropertyDescriptor(target, prop) {
let global = Cu.getGlobalForObject(this);
return wrapExceptions(global, () => {
let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop);
let desc = Reflect.getOwnPropertyDescriptor(obj, prop);
if (desc === undefined) {
return undefined;
}
// Transitively maintain the wrapper membrane.
let wrapIfExists = key => {
if (key in desc) {
desc[key] = wrapIfUnwrapped(desc[key], global);
}
};
wrapIfExists("value");
wrapIfExists("get");
wrapIfExists("set");
// A trapping proxy's properties must always be configurable, but sometimes
// we come across non-configurable properties. Tell a white lie.
desc.configurable = true;
return wrapIfUnwrapped(desc, global);
});
},
ownKeys(target) {
let props = [];
// Do the normal thing.
let wrappedObject = this.wrapped.get(target).obj;
let flt = a => !props.includes(a);
props = props.concat(Reflect.ownKeys(wrappedObject).filter(flt));
// If we've got an Xray wrapper, include the expandos as well.
if ("wrappedJSObject" in wrappedObject) {
props = props.concat(
Reflect.ownKeys(wrappedObject.wrappedJSObject).filter(flt)
);
}
return Cu.cloneInto(props, Cu.getGlobalForObject(this));
},
preventExtensions() {
throw new Error(
"Can't call preventExtensions on SpecialPowers wrapped object"
);
},
};
function wrapCallback(cb, win) {
// Do not wrap if it is already privileged.
if (!isWrappable(cb) || Cu.getObjectPrincipal(cb).isSystemPrincipal) {
return cb;
}
return function SpecialPowersCallbackWrapper() {
var args = Array.from(arguments, obj => wrapIfUnwrapped(obj, win));
let invocant = wrapIfUnwrapped(this, win);
return unwrapIfWrapped(cb.apply(invocant, args));
};
}
function wrapCallbackObject(obj, win) {
// Do not wrap if it is already privileged.
if (!isWrappable(obj) || Cu.getObjectPrincipal(obj).isSystemPrincipal) {
return obj;
}
obj = Cu.waiveXrays(obj);
var wrapper = {};
for (var i in obj) {
if (typeof obj[i] == "function") {
wrapper[i] = wrapCallback(Cu.unwaiveXrays(obj[i]), win);
} else {
wrapper[i] = obj[i];
}
}
return wrapper;
}
function disableAutoWrap(...objs) {
objs.forEach(x => noAutoWrap.add(x));
}
export var WrapPrivileged = {
wrap: wrapIfUnwrapped,
unwrap: unwrapIfWrapped,
isWrapper,
wrapCallback,
wrapCallbackObject,
disableAutoWrap,
};