385 lines
11 KiB
JavaScript
385 lines
11 KiB
JavaScript
/* 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,
|
|
};
|