summaryrefslogtreecommitdiffstats
path: root/testing/specialpowers/content/WrapPrivileged.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'testing/specialpowers/content/WrapPrivileged.sys.mjs')
-rw-r--r--testing/specialpowers/content/WrapPrivileged.sys.mjs383
1 files changed, 383 insertions, 0 deletions
diff --git a/testing/specialpowers/content/WrapPrivileged.sys.mjs b/testing/specialpowers/content/WrapPrivileged.sys.mjs
new file mode 100644
index 0000000000..5af3294ccb
--- /dev/null
+++ b/testing/specialpowers/content/WrapPrivileged.sys.mjs
@@ -0,0 +1,383 @@
+/* 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.
+ */
+
+/* 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, receiver) {
+ 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, receiver) {
+ 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(target, prop, descriptor) {
+ 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(target) {
+ 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,
+};