Browse Source

refactor: simpler/faster global wrapper

tophf 3 years ago
parent
commit
900e65f39c

+ 0 - 63
scripts/sandbox-globals.html

@@ -1,63 +0,0 @@
-<p>Run it both in Chrome and FF</p>
-<textarea id="results" style="width:30em; height: 90%" spellcheck="false"></textarea>
-
-<script>
-const el = document.getElementById('results');
-el.value = [
-  function boundMethods() {
-    const keys = [];
-    /* global globalThis */
-    for (const k in globalThis) {
-      if (k >= 'a' && k <= 'z') {
-        const { value } = Object.getOwnPropertyDescriptor(globalThis, k)
-        || Object.getOwnPropertyDescriptor(EventTarget.prototype, k)
-        || Object.getOwnPropertyDescriptor(Object.prototype, k)
-        || {};
-        if (typeof value === 'function') {
-          if (k === 'createImageBitmap' || k === 'fetch') {
-            keys.push(k);
-          } else {
-            try {
-              globalThis[k].call({});
-            } catch (e) {
-              if (/Illegal invocation|implement interface/.test(e)) {
-                keys.push(k);
-              }
-            }
-          }
-        }
-      }
-    }
-    return {
-      header: "const MAYBE = vmOwnFunc; // something that can't be imitated by the page\n",
-      keys: keys,
-      value: 'MAYBE',
-    };
-  },
-  function unforgeables() {
-    return Object.entries(Object.getOwnPropertyDescriptors(window))
-    .filter(([, v]) => !v.configurable)
-    .map(([k]) => k);
-  },
-  function readonlyKeys() {
-    return Object.entries(Object.getOwnPropertyDescriptors(window))
-    .filter(([k, v]) => k >= 'a' && k <= 'z'
-      && k !== 'webkitStorageInfo' // deprecated
-      && v.get && !v.set && v.configurable)
-    .map(([k]) => k);
-  },
-].map(fn => {
-  let res = fn();
-  if (Array.isArray(res)) res = { keys: res };
-  return `${res.header || ''}const ${fn.name} = {
-  __proto__: null,
-${
-  res.keys.sort()
-  .map(k => `  ${k}: ${res.value || 1},\n`)
-  .join('')
-}};\n`;
-}).join('\n');
-
-el.focus();
-el.select();
-</script>

+ 1 - 1
src/injected/safe-globals-injected.js

@@ -119,7 +119,7 @@ export const log = (level, ...args) => {
  * invalid props like an inherited setter when you only provide `{value}`.
  */
 export const safeDefineProperty = (obj, key, desc) => (
-  defineProperty(obj, key, createNullObj(desc))
+  defineProperty(obj, key, getPrototypeOf(desc) ? createNullObj(desc) : desc)
 );
 
 /** Unlike ::push() this one doesn't call possibly spoofed Array.prototype setters */

+ 1 - 1
src/injected/web/gm-api-wrapper.js

@@ -101,5 +101,5 @@ function makeGmMethodCaller(gmMethod, context, isAsync) {
   // keeping the native console.log intact
   if (gmMethod === gmApi.GM_log) return gmMethod;
   if (isAsync) context = assign({ __proto__: null, async: true }, context);
-  return vmOwnFunc(gmMethod::bind(context));
+  return vmOwnFunc(safeBind(gmMethod, context));
 }

+ 85 - 220
src/injected/web/gm-global-wrapper.js

@@ -1,6 +1,6 @@
 import { FastLookup, safeConcat } from './util';
 
-const isFrameIndex = key => +key >= 0 && key < window::getWindowLength();
+const CONFIGURABLE = 'configurable';
 const scopeSym = SafeSymbol.unscopables;
 const globalKeysSet = FastLookup();
 const globalKeys = (function makeGlobalKeys() {
@@ -10,7 +10,7 @@ const globalKeys = (function makeGlobalKeys() {
   const numFrames = window::getWindowLength();
   // True if `names` is usable as is, but FF is bugged: its names have duplicates
   let ok = !IS_FIREFOX;
-  names::forEach(key => {
+  for (const key of names) {
     if (+key >= 0 && key < numFrames
       || isContentMode && (
         key === process.env.INIT_FUNC_NAME || key === 'browser' || key === 'chrome'
@@ -18,142 +18,49 @@ const globalKeys = (function makeGlobalKeys() {
     ) {
       ok = false;
     } else {
-      globalKeysSet.add(key);
+      globalKeysSet.set(key, 1);
     }
-  });
+  }
   /* Chrome and FF page mode: `global` is `window`
      FF content mode: `global` is different, some props e.g. `isFinite` are defined only there */
   if (global !== window) {
     builtinGlobals[1]::forEach(key => {
       if (!(+key >= 0 && key < numFrames)) { // keep the `!` inversion to avoid safe-guarding isNaN
-        globalKeysSet.add(key);
+        globalKeysSet.set(key, -1);
         ok = false;
       }
     });
   }
   // wrappedJSObject is not included in getOwnPropertyNames so we add it explicitly.
   if (IS_FIREFOX
-    && !PAGE_MODE_HANDSHAKE
-    && kWrappedJSObject in global
-    && !globalKeysSet.has(kWrappedJSObject)) {
-    globalKeysSet.add(kWrappedJSObject);
+  && !PAGE_MODE_HANDSHAKE
+  && kWrappedJSObject in global
+  && !globalKeysSet.get(kWrappedJSObject)) {
+    globalKeysSet.set(kWrappedJSObject, 1);
     if (ok) setOwnProp(names, names.length, kWrappedJSObject);
   }
   return ok ? names : globalKeysSet.toArray();
 }());
 const inheritedKeys = createNullObj();
-/* These can be redefined but can't be assigned, see sandbox-globals.html */
-const readonlyKeys = {
-  __proto__: null,
-  applicationCache: 1,
-  caches: 1,
-  closed: 1,
-  crossOriginIsolated: 1,
-  crypto: 1,
-  customElements: 1,
-  frameElement: 1,
-  history: 1,
-  indexedDB: 1,
-  isSecureContext: 1,
-  localStorage: 1,
-  mozInnerScreenX: 1,
-  mozInnerScreenY: 1,
-  navigator: 1,
-  sessionStorage: 1,
-  speechSynthesis: 1,
-  styleMedia: 1,
-  trustedTypes: 1,
-};
-/* These can't be redefined, see sandbox-globals.html */
-const unforgeables = {
-  __proto__: null,
-  Infinity: 1,
-  NaN: 1,
-  document: 1,
-  location: 1,
-  top: 1,
-  undefined: 1,
-  window: 1,
-};
-/* ~50 methods like alert/fetch/moveBy that need `window` as `this`, see sandbox-globals.html */
-const MAYBE = vmOwnFunc; // something that can't be imitated by the page
-const boundMethods = {
-  __proto__: null,
-  addEventListener: MAYBE,
-  alert: MAYBE,
-  atob: MAYBE,
-  blur: MAYBE,
-  btoa: MAYBE,
-  cancelAnimationFrame: MAYBE,
-  cancelIdleCallback: MAYBE,
-  captureEvents: MAYBE,
-  clearInterval: MAYBE,
-  clearTimeout: MAYBE,
-  close: MAYBE,
-  confirm: MAYBE,
-  createImageBitmap: MAYBE,
-  dispatchEvent: MAYBE,
-  dump: MAYBE,
-  fetch: MAYBE,
-  find: MAYBE,
-  focus: MAYBE,
-  getComputedStyle: MAYBE,
-  getDefaultComputedStyle: MAYBE,
-  getSelection: MAYBE,
-  matchMedia: MAYBE,
-  moveBy: MAYBE,
-  moveTo: MAYBE,
-  open: MAYBE,
-  openDatabase: MAYBE,
-  postMessage: MAYBE,
-  print: MAYBE,
-  prompt: MAYBE,
-  queueMicrotask: MAYBE,
-  releaseEvents: MAYBE,
-  removeEventListener: MAYBE,
-  requestAnimationFrame: MAYBE,
-  requestIdleCallback: MAYBE,
-  resizeBy: MAYBE,
-  resizeTo: MAYBE,
-  scroll: MAYBE,
-  scrollBy: MAYBE,
-  scrollByLines: MAYBE,
-  scrollByPages: MAYBE,
-  scrollTo: MAYBE,
-  setInterval: MAYBE,
-  setResizable: MAYBE,
-  setTimeout: MAYBE,
-  sizeToContent: MAYBE,
-  stop: MAYBE,
-  updateCommands: MAYBE,
-  webkitCancelAnimationFrame: MAYBE,
-  webkitRequestAnimationFrame: MAYBE,
-  webkitRequestFileSystem: MAYBE,
-  webkitResolveLocalFileSystemURL: MAYBE,
-};
-
-if (process.env.DEBUG) throwIfProtoPresent(unforgeables);
-for (const name in unforgeables) { /* proto is null */// eslint-disable-line guard-for-in
-  let thisObj;
-  let info = (
-    describeProperty(thisObj = global, name)
-    || describeProperty(thisObj = window, name)
-  );
-  let fn;
-  if (info) {
-    info = createNullObj(info);
-    // currently only `document` and `window`
-    if ((fn = info.get)) info.get = fn::bind(thisObj);
-    // currently only `location`
-    if ((fn = info.set)) info.set = fn::bind(thisObj);
-    unforgeables[name] = info;
-  } else {
-    delete unforgeables[name];
+const globalDesc = createNullObj();
+const updateGlobalDesc = name => {
+  let src;
+  let desc;
+  if ((src = inheritedKeys[name])
+  || (src = globalKeysSet.get(name)) && (src = src > 0 ? window : global)) {
+    if ((desc = describeProperty(src, name))) {
+      desc = createNullObj(desc);
+      if (typeof name === 'string' && name[0] > 'Z' && typeof desc.value === 'function') {
+        desc.value = safeBind(desc.value, src === global ? global : window);
+      }
+      globalDesc[name] = desc;
+      return desc;
+    }
   }
-}
+};
 [SafeEventTarget, Object]::forEach(src => {
-  getOwnPropertyNames(src[PROTO])::forEach(key => {
-    inheritedKeys[key] = 1;
+  getOwnPropertyNames(src = src[PROTO])::forEach(key => {
+    inheritedKeys[key] = src;
   });
 });
 builtinGlobals = null; // eslint-disable-line no-global-assign
@@ -162,87 +69,48 @@ builtinGlobals = null; // eslint-disable-line no-global-assign
  * @desc Wrap helpers to prevent unexpected modifications.
  */
 export function makeGlobalWrapper(local) {
-  const events = createNullObj();
-  const readonlys = createNullObj(readonlyKeys);
   let globals = globalKeysSet; // will be copied only if modified
   /* Browsers may return [object Object] for Object.prototype.toString(window)
      on our `window` proxy so jQuery libs see it as a plain object and throw
      when trying to clone its recursive properties like `self` and `window`. */
   safeDefineProperty(local, toStringTagSym, { get: () => 'Window' });
+  const events = createNullObj();
   const wrapper = new SafeProxy(local, {
     __proto__: null,
     defineProperty(_, name, desc) {
-      const isStr = isString(name);
-      if (!isStr || !isFrameIndex(name)) {
-        safeDefineProperty(local, name, desc);
-        if (isStr) setEventHandler(name);
-        delete readonlys[name];
+      if (name in local
+      || !(_ = globalDesc[name] || updateGlobalDesc(name))
+      || _[CONFIGURABLE]) {
+        return safeDefineProperty(local, name, desc);
       }
-      return true;
     },
     deleteProperty(_, name) {
-      if (!(name in unforgeables) && delete local[name]) {
-        if (globals.has(name)) {
-          if (globals === globalKeysSet) {
-            globals = globalKeysSet.clone();
-          }
-          globals.delete(name);
+      if ((_ = delete local[name])
+      && (_ = globalDesc[name] || updateGlobalDesc(name))
+      && (_ = _[CONFIGURABLE])) {
+        if (globals === globalKeysSet) {
+          globals = globalKeysSet.clone();
         }
-        return true;
+        globals.delete(name);
       }
+      return !!_;
     },
-    // Reducing "steppability" so it doesn't get in the way of debugging other parts of our code.
-    // eslint-disable-next-line no-return-assign, no-nested-ternary
-    get: (_, name) => (name === 'undefined' || name === scopeSym ? undefined
-      : (_ = local[name]) !== undefined || name in local ? _
-        : resolveProp(name, wrapper, globals, local)
-    ),
-    getOwnPropertyDescriptor(_, name) {
-      const ownDesc = describeProperty(local, name);
-      const desc = ownDesc || globals.has(name) && describeProperty(global, name);
-      if (!desc) return;
-      if (getOwnProp(desc, 'value') === window) {
-        desc.value = wrapper;
-      }
-      // preventing spec violation - we must mirror an unknown unforgeable prop
-      // preventing error for libs like ReactDOM that hook window.event
-      if (!ownDesc && (!getOwnProp(desc, 'configurable') || name === 'event')) {
-        const get = getOwnProp(desc, 'get');
-        const set = getOwnProp(desc, 'set'); // for window.event
-        if (get) desc.get = get::bind(global);
-        if (set) desc.set = set::bind(global);
-        safeDefineProperty(local, name, desc);
-      }
-      return desc;
+    get: (_, name) => {
+      if (name === 'undefined' || name === scopeSym) return;
+      if ((_ = local[name]) !== undefined || name in local) return _;
+      return proxyDescribe(local, name, wrapper, events) && local[name];
     },
-    has: (_, name) => name in local || name in inheritedKeys || globals.has(name),
+    getOwnPropertyDescriptor: (_, name) => describeProperty(local, name)
+      || proxyDescribe(local, name, wrapper, events),
+    has: (_, name) => name in globalDesc || name in local || updateGlobalDesc(name),
     ownKeys: () => makeOwnKeys(local, globals),
     preventExtensions() {},
     set(_, name, value) {
-      const isStr = isString(name);
-      let readonly = readonlys[name];
-      if (readonly === 1) {
-        readonly = globals.has(name) ? 2 : 0;
-        readonlys[name] = readonly;
-      }
-      if (!readonly && (!isStr || !isFrameIndex(name))) {
-        local[name] = value;
-        if (isStr) setEventHandler(name, value, globals, events);
-      }
+      if (!(name in local)) proxyDescribe(local, name, wrapper, events);
+      local[name] = value;
       return true;
     },
   });
-  for (const name in unforgeables) { /* proto is null */// eslint-disable-line guard-for-in
-    const desc = unforgeables[name];
-    if (name === 'window' || name === 'top' && IS_TOP) {
-      delete desc.get;
-      delete desc.set;
-      desc.value = wrapper;
-    }
-    if (process.env.DEBUG) throwIfProtoPresent(desc);
-    /* proto is already null */// eslint-disable-next-line no-restricted-syntax
-    defineProperty(local, name, desc);
-  }
   return wrapper;
 }
 
@@ -250,8 +118,8 @@ function makeOwnKeys(local, globals) {
   /** Note that arrays can be eavesdropped via prototype setters like '0','1',...
    * on `push` and `arr[i] = 123`, as well as via getters if you read beyond
    * its length or from an unassigned `hole`. */
-  const names = getOwnPropertyNames(local)::filter(notIncludedIn, globals);
-  const symbols = getOwnPropertySymbols(local)::filter(notIncludedIn, globals);
+  const names = getOwnPropertyNames(local)::filter(notIncludedIn, globals.get);
+  const symbols = getOwnPropertySymbols(local)::filter(notIncludedIn, globals.get);
   const frameIndexes = [];
   for (let i = 0, len = window::getWindowLength(); i < len && window::hasOwnProperty(i); i += 1) {
     if (!(i in local)) {
@@ -266,48 +134,45 @@ function makeOwnKeys(local, globals) {
   );
 }
 
-function resolveProp(name, wrapper, globals, local) {
-  let value = boundMethods[name];
-  if (value === MAYBE) {
-    value = window[name];
-    if (isFunction(value)) {
-      value = value::bind(window);
-    }
-    boundMethods[name] = value;
-  }
-  const canCopy = value || name in inheritedKeys || globals.has(name);
-  if (!value && (canCopy || isString(name) && isFrameIndex(name))) {
-    value = global[name];
-  }
-  if (value === window || name === 'globalThis') {
-    value = wrapper;
-  }
-  if (canCopy && (
-    isFunction(value) || isObject(value) && name !== 'event'
-    // window.event contains the current event so it's always different
-  )) {
-    local[name] = value;
-  }
-  return value;
-}
-
-function setEventHandler(name, value, globals, events) {
-  // Spoofed String index getters won't be called within length, length itself is unforgeable
-  if (name.length < 3 || name[0] !== 'o' || name[1] !== 'n' || !globals.has(name)) {
-    return;
-  }
-  name = name::slice(2);
-  window::off(name, events[name]);
-  if (isFunction(value)) {
-    // the handler will be unique so that one script couldn't remove something global
-    // like console.log set by another script
-    window::on(name, events[name] = value::bind(window));
+function proxyDescribe(local, name, wrapper, events) {
+  let desc = globalDesc[name] || updateGlobalDesc(name);
+  if (!desc) return;
+  const { get, set, value } = desc;
+  const isWindow = value === window
+    || name === 'window'
+    || name === 'self'
+    || name === 'globalThis'
+    || name === 'top' && window === top
+    || name === 'parent' && window === parent;
+  if (isWindow) {
+    desc.value = wrapper;
+    delete desc.get;
+    delete desc.set;
+  } else if (get && set && typeof name === 'string'
+    // Spoofed String index getters won't be called within length, length itself is unforgeable
+    && name.length >= 3 && name[0] === 'o' && name[1] === 'n'
+  ) {
+    name = name::slice(2);
+    desc.get = () => events[name] || null;
+    desc.set = fn => {
+      window::off(name, events[name]);
+      if (isFunction(fn)) {
+        // the handler will be unique so that one script couldn't remove something global
+        // like console.log set by another script
+        window::on(name, events[name] = safeBind(fn, wrapper));
+      } else {
+        delete events[name];
+      }
+    };
   } else {
-    delete events[name];
+    if (get) desc.get = safeBind(get, window);
+    if (set) desc.set = safeBind(set, window);
   }
+  defineProperty(local, name, desc); /* proto is null */// eslint-disable-line no-restricted-syntax
+  return desc;
 }
 
-/** @this {FastLookup|Set} */
+/** @this {FastLookup.get} */
 function notIncludedIn(key) {
-  return !this.has(key);
+  return !this(key);
 }

+ 5 - 4
src/injected/web/safe-globals-web.js

@@ -33,11 +33,11 @@ export let
   // Object
   apply,
   assign,
-  bind,
   defineProperty,
   describeProperty,
   getOwnPropertyNames,
   getOwnPropertySymbols,
+  getPrototypeOf,
   objectKeys,
   objectValues,
   // Object.prototype
@@ -56,6 +56,7 @@ export let
   charCodeAt,
   slice,
   // safeCall
+  safeBind,
   safeCall,
   // various values
   builtinGlobals,
@@ -84,7 +85,6 @@ export let
  * or window[0] before our content script runs at document_start, https://crbug.com/1261964 */
 export const VAULT = (() => {
   let ArrayP;
-  let ElementP;
   let SafeObject;
   let StringP;
   let i = -1;
@@ -128,11 +128,11 @@ export const VAULT = (() => {
     describeProperty = res[i += 1] || SafeObject.getOwnPropertyDescriptor,
     getOwnPropertyNames = res[i += 1] || SafeObject.getOwnPropertyNames,
     getOwnPropertySymbols = res[i += 1] || SafeObject.getOwnPropertySymbols,
+    getPrototypeOf = res[i += 1] || SafeObject.getPrototypeOf,
     assign = res[i += 1] || SafeObject.assign,
     objectKeys = res[i += 1] || SafeObject.keys,
     objectValues = res[i += 1] || SafeObject.values,
     apply = res[i += 1] || SafeObject.apply,
-    bind = res[i += 1] || SafeObject.bind,
     // Object.prototype
     hasOwnProperty = res[i += 1] || SafeObject[PROTO].hasOwnProperty,
     objectToString = res[i += 1] || SafeObject[PROTO].toString,
@@ -142,12 +142,13 @@ export const VAULT = (() => {
     forEach = res[i += 1] || ArrayP.forEach,
     indexOf = res[i += 1] || ArrayP.indexOf,
     // Element.prototype
-    remove = res[i += 1] || (ElementP = src.Element[PROTO]).remove,
+    remove = res[i += 1] || src.Element[PROTO].remove,
     // String.prototype
     charCodeAt = res[i += 1] || (StringP = src.String[PROTO]).charCodeAt,
     slice = res[i += 1] || StringP.slice,
     // safeCall
     safeCall = res[i += 1] || (call = SafeObject.call).bind(call),
+    safeBind = res[i += 1] || call.bind(SafeObject.bind),
     // various methods
     URLToString = res[i += 1] || src.URL[PROTO].toString,
     createObjectURL = res[i += 1] || src.URL.createObjectURL,

+ 5 - 9
src/injected/web/util.js

@@ -58,9 +58,6 @@ export const jsonDump = (value, stack) => {
 export const FastLookup = (hubs = createNullObj()) => {
   /** @namespace FastLookup */
   return {
-    add(val) {
-      getHub(val, true)[val] = true;
-    },
     clone() {
       const clone = createNullObj();
       if (process.env.DEBUG) throwIfProtoPresent(clone);
@@ -69,18 +66,17 @@ export const FastLookup = (hubs = createNullObj()) => {
       }
       return FastLookup(clone);
     },
-    delete(val) {
-      delete getHub(val)?.[val];
-    },
-    has: val => getHub(val)?.[val],
+    delete: key => delete getHub(key)?.[key],
+    get: key => getHub(key)?.[key],
+    set: (key, val) => (getHub(key, true)[key] = val),
     toArray: () => {
       const values = objectValues(hubs);
       values::forEach((val, i) => { values[i] = objectKeys(val); });
       return safeConcat::apply(null, values);
     },
   };
-  function getHub(val, autoCreate) {
-    const group = val.length ? val[0] : ''; // length is unforgeable, index getters aren't
+  function getHub(key, autoCreate) {
+    const group = key.length ? key[0] : ''; // length is unforgeable, index getters aren't
     const hub = hubs[group] || (
       autoCreate ? (hubs[group] = createNullObj())
         : null