tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

WrapPrivileged.sys.mjs (11547B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * This module handles wrapping privileged objects so that they can be exposed
      7 * to unprivileged contexts. It is only to be used in automated tests.
      8 *
      9 * Its exact semantics are also liable to change at any time, so any callers
     10 * relying on undocumented behavior or subtle platform features should expect
     11 * breakage. Those callers should, wherever possible, migrate to fully
     12 * chrome-privileged scripts when they need to interact with privileged APIs.
     13 */
     14 
     15 // XPCNativeWrapper is not defined globally in ESLint as it may be going away.
     16 // See bug 1481337.
     17 /* globals XPCNativeWrapper */
     18 
     19 Cu.crashIfNotInAutomation();
     20 
     21 let wrappedObjects = new WeakMap();
     22 let perWindowInfo = new WeakMap();
     23 let noAutoWrap = new WeakSet();
     24 
     25 function isWrappable(x) {
     26  if (typeof x === "object") {
     27    return x !== null;
     28  }
     29  return typeof x === "function";
     30 }
     31 
     32 function isWrapper(x) {
     33  try {
     34    return isWrappable(x) && wrappedObjects.has(x);
     35  } catch (e) {
     36    // If `x` is a remote object proxy, trying to access an unexpected property
     37    // on it will throw a security error, even though we're chrome privileged.
     38    // However, remote proxies are not SpecialPowers wrappers, so:
     39    return false;
     40  }
     41 }
     42 
     43 function unwrapIfWrapped(x) {
     44  return isWrapper(x) ? unwrapPrivileged(x) : x;
     45 }
     46 
     47 function wrapIfUnwrapped(x, w) {
     48  return isWrapper(x) ? x : wrapPrivileged(x, w);
     49 }
     50 
     51 function isObjectOrArray(obj) {
     52  if (Object(obj) !== obj) {
     53    return false;
     54  }
     55  let arrayClasses = [
     56    "Object",
     57    "Array",
     58    "Int8Array",
     59    "Uint8Array",
     60    "Int16Array",
     61    "Uint16Array",
     62    "Int32Array",
     63    "Uint32Array",
     64    "Float32Array",
     65    "Float64Array",
     66    "Uint8ClampedArray",
     67  ];
     68  let className = Cu.getClassName(obj, true);
     69  return arrayClasses.includes(className);
     70 }
     71 
     72 // In general, we want Xray wrappers for content DOM objects, because waiving
     73 // Xray gives us Xray waiver wrappers that clamp the principal when we cross
     74 // compartment boundaries. However, there are some exceptions where we want
     75 // to use a waiver:
     76 //
     77 // * Xray adds some gunk to toString(), which has the potential to confuse
     78 //   consumers that aren't expecting Xray wrappers. Since toString() is a
     79 //   non-privileged method that returns only strings, we can just waive Xray
     80 //   for that case.
     81 //
     82 // * We implement Xrays to pure JS [[Object]] and [[Array]] instances that
     83 //   filter out tricky things like callables. This is the right thing for
     84 //   security in general, but tends to break tests that try to pass object
     85 //   literals into SpecialPowers. So we waive [[Object]] and [[Array]]
     86 //   instances before inspecting properties.
     87 //
     88 // * When we don't have meaningful Xray semantics, we create an Opaque
     89 //   XrayWrapper for security reasons. For test code, we generally want to see
     90 //   through that sort of thing.
     91 function waiveXraysIfAppropriate(obj, propName) {
     92  if (
     93    propName == "toString" ||
     94    isObjectOrArray(obj) ||
     95    /Opaque/.test(Object.prototype.toString.call(obj))
     96  ) {
     97    return XPCNativeWrapper.unwrap(obj);
     98  }
     99  return obj;
    100 }
    101 
    102 // We can't call apply() directy on Xray-wrapped functions, so we have to be
    103 // clever.
    104 function doApply(fun, invocant, args) {
    105  // We implement Xrays to pure JS [[Object]] instances that filter out tricky
    106  // things like callables. This is the right thing for security in general,
    107  // but tends to break tests that try to pass object literals into
    108  // SpecialPowers. So we waive [[Object]] instances when they're passed to a
    109  // SpecialPowers-wrapped callable.
    110  //
    111  // Note that the transitive nature of Xray waivers means that any property
    112  // pulled off such an object will also be waived, and so we'll get principal
    113  // clamping for Xrayed DOM objects reached from literals, so passing things
    114  // like {l : xoWin.location} won't work. Hopefully the rabbit hole doesn't
    115  // go that deep.
    116  args = args.map(x => (isObjectOrArray(x) ? Cu.waiveXrays(x) : x));
    117  return Reflect.apply(fun, invocant, args);
    118 }
    119 
    120 function wrapPrivileged(obj, win) {
    121  // Primitives pass straight through.
    122  if (!isWrappable(obj)) {
    123    return obj;
    124  }
    125 
    126  // No double wrapping.
    127  if (isWrapper(obj)) {
    128    throw new Error("Trying to double-wrap object!");
    129  }
    130 
    131  let { windowID, proxies, handler } = perWindowInfo.get(win) || {};
    132  // |windowUtils| is undefined if |win| is a non-window object
    133  // such as a sandbox.
    134  let currentID = win.windowGlobalChild
    135    ? win.windowGlobalChild.innerWindowId
    136    : 0;
    137  // Values are dead objects if the inner window is changed.
    138  if (windowID !== currentID) {
    139    windowID = currentID;
    140    proxies = new WeakMap();
    141    handler = Cu.cloneInto(SpecialPowersHandler, win, {
    142      cloneFunctions: true,
    143    });
    144    handler.wrapped = new win.WeakMap();
    145    perWindowInfo.set(win, { windowID, proxies, handler });
    146  }
    147 
    148  if (proxies.has(obj)) {
    149    return proxies.get(obj).proxy;
    150  }
    151 
    152  let className = Cu.getClassName(obj, true);
    153  if (className === "ArrayBuffer") {
    154    // Since |new Uint8Array(<proxy>)| doesn't work as expected, we have to
    155    // return a real ArrayBuffer.
    156    return obj instanceof win.ArrayBuffer ? obj : Cu.cloneInto(obj, win);
    157  }
    158 
    159  let dummy;
    160  if (typeof obj === "function") {
    161    dummy = Cu.exportFunction(function () {}, win);
    162  } else {
    163    dummy = new win.Object();
    164  }
    165  handler.wrapped.set(dummy, { obj });
    166 
    167  let proxy = new win.Proxy(dummy, handler);
    168  wrappedObjects.set(proxy, obj);
    169  switch (className) {
    170    case "AnonymousContent":
    171      // Caching anonymous content will cause crashes (bug 1636015).
    172      break;
    173    case "CSSStyleProperties":
    174    case "CSSStyleRule":
    175    case "CSSStyleSheet":
    176      // Caching these classes will cause memory leaks.
    177      break;
    178    default:
    179      proxies.set(obj, { proxy });
    180      break;
    181  }
    182  return proxy;
    183 }
    184 
    185 function unwrapPrivileged(x) {
    186  // We don't wrap primitives, so sometimes we have a primitive where we'd
    187  // expect to have a wrapper. The proxy pretends to be the type that it's
    188  // emulating, so we can just as easily check isWrappable() on a proxy as
    189  // we can on an unwrapped object.
    190  if (!isWrappable(x)) {
    191    return x;
    192  }
    193 
    194  // If we have a wrappable type, make sure it's wrapped.
    195  if (!isWrapper(x)) {
    196    throw new Error("Trying to unwrap a non-wrapped object!");
    197  }
    198 
    199  // unwrapped.
    200  return wrappedObjects.get(x);
    201 }
    202 
    203 function wrapExceptions(global, fn) {
    204  try {
    205    return fn();
    206  } catch (e) {
    207    throw wrapIfUnwrapped(e, global);
    208  }
    209 }
    210 
    211 let SpecialPowersHandler = {
    212  construct(target, args) {
    213    // The arguments may or may not be wrappers. Unwrap them if necessary.
    214    var unwrappedArgs = Array.from(Cu.waiveXrays(args), x =>
    215      unwrapIfWrapped(Cu.unwaiveXrays(x))
    216    );
    217 
    218    // We want to invoke "obj" as a constructor, but using unwrappedArgs as
    219    // the arguments.
    220    let global = Cu.getGlobalForObject(this);
    221    return wrapExceptions(global, () =>
    222      wrapIfUnwrapped(
    223        Reflect.construct(this.wrapped.get(target).obj, unwrappedArgs),
    224        global
    225      )
    226    );
    227  },
    228 
    229  apply(target, thisValue, args) {
    230    let wrappedObject = this.wrapped.get(target).obj;
    231    let global = Cu.getGlobalForObject(this);
    232    // The invocant and arguments may or may not be wrappers. Unwrap
    233    // them if necessary.
    234    var invocant = unwrapIfWrapped(thisValue);
    235 
    236    return wrapExceptions(global, () => {
    237      if (noAutoWrap.has(wrappedObject)) {
    238        args = Array.from(Cu.waiveXrays(args), x => Cu.unwaiveXrays(x));
    239        return doApply(wrappedObject, invocant, args);
    240      }
    241 
    242      if (wrappedObject.name == "then") {
    243        args = Array.from(Cu.waiveXrays(args), x =>
    244          wrapCallback(Cu.unwaiveXrays(x), global)
    245        );
    246      } else {
    247        args = Array.from(Cu.waiveXrays(args), x =>
    248          unwrapIfWrapped(Cu.unwaiveXrays(x))
    249        );
    250      }
    251 
    252      return wrapIfUnwrapped(doApply(wrappedObject, invocant, args), global);
    253    });
    254  },
    255 
    256  has(target, prop) {
    257    return Reflect.has(this.wrapped.get(target).obj, prop);
    258  },
    259 
    260  get(target, prop) {
    261    let global = Cu.getGlobalForObject(this);
    262    return wrapExceptions(global, () => {
    263      let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop);
    264      let val = Reflect.get(obj, prop);
    265      return wrapIfUnwrapped(val, global);
    266    });
    267  },
    268 
    269  set(target, prop, val) {
    270    return wrapExceptions(Cu.getGlobalForObject(this), () => {
    271      let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop);
    272      return Reflect.set(obj, prop, unwrapIfWrapped(val));
    273    });
    274  },
    275 
    276  delete(target, prop) {
    277    return wrapExceptions(Cu.getGlobalForObject(this), () => {
    278      return Reflect.deleteProperty(this.wrapped.get(target).obj, prop);
    279    });
    280  },
    281 
    282  defineProperty() {
    283    throw new Error(
    284      "Can't call defineProperty on SpecialPowers wrapped object"
    285    );
    286  },
    287 
    288  getOwnPropertyDescriptor(target, prop) {
    289    let global = Cu.getGlobalForObject(this);
    290    return wrapExceptions(global, () => {
    291      let obj = waiveXraysIfAppropriate(this.wrapped.get(target).obj, prop);
    292      let desc = Reflect.getOwnPropertyDescriptor(obj, prop);
    293 
    294      if (desc === undefined) {
    295        return undefined;
    296      }
    297 
    298      // Transitively maintain the wrapper membrane.
    299      let wrapIfExists = key => {
    300        if (key in desc) {
    301          desc[key] = wrapIfUnwrapped(desc[key], global);
    302        }
    303      };
    304 
    305      wrapIfExists("value");
    306      wrapIfExists("get");
    307      wrapIfExists("set");
    308 
    309      // A trapping proxy's properties must always be configurable, but sometimes
    310      // we come across non-configurable properties. Tell a white lie.
    311      desc.configurable = true;
    312 
    313      return wrapIfUnwrapped(desc, global);
    314    });
    315  },
    316 
    317  ownKeys(target) {
    318    let props = [];
    319 
    320    // Do the normal thing.
    321    let wrappedObject = this.wrapped.get(target).obj;
    322    let flt = a => !props.includes(a);
    323    props = props.concat(Reflect.ownKeys(wrappedObject).filter(flt));
    324 
    325    // If we've got an Xray wrapper, include the expandos as well.
    326    if ("wrappedJSObject" in wrappedObject) {
    327      props = props.concat(
    328        Reflect.ownKeys(wrappedObject.wrappedJSObject).filter(flt)
    329      );
    330    }
    331 
    332    return Cu.cloneInto(props, Cu.getGlobalForObject(this));
    333  },
    334 
    335  preventExtensions() {
    336    throw new Error(
    337      "Can't call preventExtensions on SpecialPowers wrapped object"
    338    );
    339  },
    340 };
    341 
    342 function wrapCallback(cb, win) {
    343  // Do not wrap if it is already privileged.
    344  if (!isWrappable(cb) || Cu.getObjectPrincipal(cb).isSystemPrincipal) {
    345    return cb;
    346  }
    347  return function SpecialPowersCallbackWrapper() {
    348    var args = Array.from(arguments, obj => wrapIfUnwrapped(obj, win));
    349    let invocant = wrapIfUnwrapped(this, win);
    350    return unwrapIfWrapped(cb.apply(invocant, args));
    351  };
    352 }
    353 
    354 function wrapCallbackObject(obj, win) {
    355  // Do not wrap if it is already privileged.
    356  if (!isWrappable(obj) || Cu.getObjectPrincipal(obj).isSystemPrincipal) {
    357    return obj;
    358  }
    359  obj = Cu.waiveXrays(obj);
    360  var wrapper = {};
    361  for (var i in obj) {
    362    if (typeof obj[i] == "function") {
    363      wrapper[i] = wrapCallback(Cu.unwaiveXrays(obj[i]), win);
    364    } else {
    365      wrapper[i] = obj[i];
    366    }
    367  }
    368  return wrapper;
    369 }
    370 
    371 function disableAutoWrap(...objs) {
    372  objs.forEach(x => noAutoWrap.add(x));
    373 }
    374 
    375 export var WrapPrivileged = {
    376  wrap: wrapIfUnwrapped,
    377  unwrap: unwrapIfWrapped,
    378 
    379  isWrapper,
    380 
    381  wrapCallback,
    382  wrapCallbackObject,
    383 
    384  disableAutoWrap,
    385 };