瀏覽代碼

fix: guard webpack's bootstrap + window.open()

fixes #1400 as much as we probably can
tophf 4 年之前
父節點
當前提交
f6f351f464

+ 1 - 0
.eslintignore

@@ -5,5 +5,6 @@
 !scripts/manifest-helper.js
 !scripts/manifest-helper.js
 !scripts/plaid.conf.js
 !scripts/plaid.conf.js
 !scripts/webpack.conf.js
 !scripts/webpack.conf.js
+!scripts/webpack-protect-bootstrap-plugin.js
 !gulpfile.js
 !gulpfile.js
 src/public/**
 src/public/**

+ 62 - 0
scripts/webpack-protect-bootstrap-plugin.js

@@ -0,0 +1,62 @@
+const escapeStringRegexp = require('escape-string-regexp');
+
+/**
+ * WARNING! The following globals must be correctly assigned using wrapper-webpack-plugin.
+ * toStringTag = Symbol.toStringTag
+ * defineProperty = Object.defineProperty
+ * hasOwnProperty = Object.prototype.hasOwnProperty
+ * safeCall = Function.prototype.call.bind(Function.prototype.call)
+ */
+class WebpackProtectBootstrapPlugin {
+  apply(compiler) {
+    const NAME = this.constructor.name;
+    const NULL_PROTO = '__proto__: null';
+    const NULL_OBJ = `{ ${NULL_PROTO} }`;
+    compiler.hooks.compilation.tap(NAME, (compilation) => {
+      const { hooks, requireFn } = compilation.mainTemplate;
+      hooks.localVars.tap(NAME, src => replace(src, [[
+        'installedModules = {};',
+        `installedModules = ${NULL_OBJ}; \
+         for (let i = 0, c, str = "cdmnoprt"; i < str.length && (c = str[i++]);) \
+           defineProperty(${requireFn}, c, { value: undefined, writable: true });`,
+      ]]));
+      hooks.moduleObj.tap(NAME, src => replace(src, [[
+        'exports: {}',
+        `exports: ${NULL_OBJ}, ${NULL_PROTO}`,
+      ]]));
+      hooks.require.tap(NAME, src => replace(src, [[
+        'modules[moduleId].call(',
+        'safeCall(modules[moduleId], ',
+      ]]));
+      hooks.requireExtensions.tap(NAME, src => replace(src, [
+        ["(typeof Symbol !== 'undefined' && Symbol.toStringTag)", '(true)'],
+        ['Symbol.toStringTag', 'toStringTag'],
+        ['Object.defineProperty', 'defineProperty'],
+        ['Object.create(null)', NULL_OBJ],
+        ['for(var key in value)', 'for(const key in value)'],
+        ['function(key) { return value[key]; }.bind(null, key)',
+          '() => value[key]'],
+        [/function[^{]+{[^}]+?hasOwnProperty\.call[^}]+}/g,
+          '(obj, key) => safeCall(hasOwnProperty, obj, key)'],
+      ]));
+    });
+  }
+}
+
+function replace(src, fromTo) {
+  const origSrc = src;
+  for (const [from, to] of fromTo) {
+    const fromRe = typeof from === 'string'
+      ? new RegExp(escapeStringRegexp(from), 'g')
+      : from;
+    const dst = src.replace(fromRe, to);
+    if (dst === src) {
+      throw new Error(`${WebpackProtectBootstrapPlugin.constructor.name}: `
+        + `"${from}" not found in "${origSrc}"`);
+    }
+    src = dst;
+  }
+  return src;
+}
+
+module.exports = WebpackProtectBootstrapPlugin;

+ 7 - 3
scripts/webpack.conf.js

@@ -7,6 +7,7 @@ const HTMLInlineCSSWebpackPlugin = isProd && require('html-inline-css-webpack-pl
 const TerserPlugin = isProd && require('terser-webpack-plugin');
 const TerserPlugin = isProd && require('terser-webpack-plugin');
 const deepmerge = isProd && require('deepmerge');
 const deepmerge = isProd && require('deepmerge');
 const { ListBackgroundScriptsPlugin } = require('./manifest-helper');
 const { ListBackgroundScriptsPlugin } = require('./manifest-helper');
+const ProtectWebpackBootstrapPlugin = require('./webpack-protect-bootstrap-plugin');
 const projectConfig = require('./plaid.conf');
 const projectConfig = require('./plaid.conf');
 const mergedConfig = shallowMerge(defaultOptions, projectConfig);
 const mergedConfig = shallowMerge(defaultOptions, projectConfig);
 
 
@@ -81,6 +82,7 @@ const defsObj = {
     { key: 'SYNC_ONEDRIVE_CLIENT_SECRET' },
     { key: 'SYNC_ONEDRIVE_CLIENT_SECRET' },
   ]),
   ]),
   'process.env.INIT_FUNC_NAME': JSON.stringify(INIT_FUNC_NAME),
   'process.env.INIT_FUNC_NAME': JSON.stringify(INIT_FUNC_NAME),
+  'process.env.VAULT_ID_NAME': JSON.stringify(VAULT_ID),
   'process.env.VAULT_ID': VAULT_ID,
   'process.env.VAULT_ID': VAULT_ID,
 };
 };
 const defsRe = new RegExp(`\\b(${Object.keys(defsObj).join('|').replace(/\./g, '\\.')})\\b`, 'g');
 const defsRe = new RegExp(`\\b(${Object.keys(defsObj).join('|').replace(/\./g, '\\.')})\\b`, 'g');
@@ -175,6 +177,7 @@ module.exports = Promise.all([
   }),
   }),
 
 
   modify('injected', './src/injected', (config) => {
   modify('injected', './src/injected', (config) => {
+    config.plugins.push(new ProtectWebpackBootstrapPlugin());
     addWrapper(config, 'injected/content', getGlobals => ({
     addWrapper(config, 'injected/content', getGlobals => ({
       header: () => `${skipReinjectionHeader} { ${getGlobals()}`,
       header: () => `${skipReinjectionHeader} { ${getGlobals()}`,
       footer: '}',
       footer: '}',
@@ -184,14 +187,15 @@ module.exports = Promise.all([
   modify('injected-web', './src/injected/web', (config) => {
   modify('injected-web', './src/injected/web', (config) => {
     // TODO: replace WebPack's Object.*, .call(), .apply() with safe calls
     // TODO: replace WebPack's Object.*, .call(), .apply() with safe calls
     config.output.libraryTarget = 'commonjs2';
     config.output.libraryTarget = 'commonjs2';
+    config.plugins.push(new ProtectWebpackBootstrapPlugin());
     addWrapper(config, 'injected/web', getGlobals => ({
     addWrapper(config, 'injected/web', getGlobals => ({
       header: () => `${skipReinjectionHeader}
       header: () => `${skipReinjectionHeader}
         window['${INIT_FUNC_NAME}'] = function (${VAULT_ID}, IS_FIREFOX) {
         window['${INIT_FUNC_NAME}'] = function (${VAULT_ID}, IS_FIREFOX) {
-          var module = { exports: {} };
+          const module = { __proto__: null };
           ${getGlobals()}`,
           ${getGlobals()}`,
       footer: `
       footer: `
-          module = module.exports;
-          return module.__esModule ? module.default : module;
+          const { exports } = module;
+          return exports.__esModule ? exports.default : exports;
         };0;`,
         };0;`,
     }));
     }));
   }),
   }),

+ 2 - 3
src/common/consts.js

@@ -1,12 +1,11 @@
-/* SAFETY WARNING! Exports used by `injected` must make ::safe() calls,
-   when accessed after the initial event loop task in `injected/web`
-   or after the first content-mode userscript runs in `injected/content` */
+// SAFETY WARNING! Exports used by `injected` must make ::safe() calls
 
 
 export const INJECT_AUTO = 'auto';
 export const INJECT_AUTO = 'auto';
 export const INJECT_PAGE = 'page';
 export const INJECT_PAGE = 'page';
 export const INJECT_CONTENT = 'content';
 export const INJECT_CONTENT = 'content';
 
 
 export const INJECT_MAPPING = {
 export const INJECT_MAPPING = {
+  __proto__: null,
   // `auto` tries to provide `window` from the real page as `unsafeWindow`
   // `auto` tries to provide `window` from the real page as `unsafeWindow`
   [INJECT_AUTO]: [INJECT_PAGE, INJECT_CONTENT],
   [INJECT_AUTO]: [INJECT_PAGE, INJECT_CONTENT],
   // inject into page context
   // inject into page context

+ 1 - 3
src/common/index.js

@@ -1,6 +1,4 @@
-/* SAFETY WARNING! Exports used by `injected` must make ::safe() calls,
-   when accessed after the initial event loop task in `injected/web`
-   or after the first content-mode userscript runs in `injected/content` */
+// SAFETY WARNING! Exports used by `injected` must make ::safe() calls
 
 
 import { browser } from '#/common/consts';
 import { browser } from '#/common/consts';
 import { deepCopy } from './object';
 import { deepCopy } from './object';

+ 1 - 3
src/common/object.js

@@ -1,6 +1,4 @@
-/* SAFETY WARNING! Exports used by `injected` must make ::safe() calls,
-   when accessed after the initial event loop task in `injected/web`
-   or after the first content-mode userscript runs in `injected/content` */
+// SAFETY WARNING! Exports used by `injected` must make ::safe() calls
 
 
 const {
 const {
   entries: objectEntries,
   entries: objectEntries,

+ 9 - 15
src/common/util.js

@@ -1,14 +1,7 @@
-/* SAFETY WARNING! Exports used by `injected` must make ::safe() calls,
-   when accessed after the initial event loop task in `injected/web`
-   or after the first content-mode userscript runs in `injected/content` */
+// SAFETY WARNING! Exports used by `injected` must make ::safe() calls
 
 
 import { browser } from '#/common/consts';
 import { browser } from '#/common/consts';
 
 
-// used in an unsafe context so we need to save the original functions
-const perfNow = performance.now.bind(performance);
-const { random, floor } = Math;
-const { toString: numberToString } = 0;
-
 export const isPromise = val => val::objectToString() === '[object Promise]';
 export const isPromise = val => val::objectToString() === '[object Promise]';
 export const isFunction = val => typeof val === 'function';
 export const isFunction = val => typeof val === 'function';
 
 
@@ -46,17 +39,17 @@ export function debounce(func, time) {
   time = Math.max(0, +time || 0);
   time = Math.max(0, +time || 0);
   function checkTime() {
   function checkTime() {
     timer = null;
     timer = null;
-    if (perfNow() >= startTime) callback();
+    if (performance.now() >= startTime) callback();
     else checkTimer();
     else checkTimer();
   }
   }
   function checkTimer() {
   function checkTimer() {
     if (!timer) {
     if (!timer) {
-      const delta = startTime - perfNow();
+      const delta = startTime - performance.now();
       timer = setTimeout(checkTime, delta);
       timer = setTimeout(checkTime, delta);
     }
     }
   }
   }
   function debouncedFunction(...args) {
   function debouncedFunction(...args) {
-    startTime = perfNow() + time;
+    startTime = performance.now() + time;
     callback = () => {
     callback = () => {
       callback = null;
       callback = null;
       func.apply(this, args);
       func.apply(this, args);
@@ -70,7 +63,7 @@ export function throttle(func, time) {
   let lastTime = 0;
   let lastTime = 0;
   time = Math.max(0, +time || 0);
   time = Math.max(0, +time || 0);
   function throttledFunction(...args) {
   function throttledFunction(...args) {
-    const now = perfNow();
+    const now = performance.now();
     if (lastTime + time < now) {
     if (lastTime + time < now) {
       lastTime = now;
       lastTime = now;
       func.apply(this, args);
       func.apply(this, args);
@@ -82,10 +75,10 @@ export function throttle(func, time) {
 export function noop() {}
 export function noop() {}
 
 
 export function getUniqId(prefix = 'VM') {
 export function getUniqId(prefix = 'VM') {
-  const now = perfNow();
+  const now = performance.now();
   return prefix
   return prefix
-    + floor((now - floor(now)) * 1e12)::numberToString(36)
-    + floor(random() * 1e12)::numberToString(36);
+    + Math.floor((now - Math.floor(now)) * 1e12).toString(36)
+    + Math.floor(Math.random() * 1e12).toString(36);
 }
 }
 
 
 /**
 /**
@@ -313,6 +306,7 @@ export async function request(url, options = {}) {
 }
 }
 
 
 const SIMPLE_VALUE_TYPE = {
 const SIMPLE_VALUE_TYPE = {
+  __proto__: null,
   string: 's',
   string: 's',
   number: 'n',
   number: 'n',
   boolean: 'b',
   boolean: 'b',

+ 5 - 4
src/injected/content/index.js

@@ -1,4 +1,4 @@
-import { getUniqId, isEmpty, sendCmd } from '#/common';
+import { isEmpty, sendCmd } from '#/common';
 import { INJECT_CONTENT } from '#/common/consts';
 import { INJECT_CONTENT } from '#/common/consts';
 import bridge from './bridge';
 import bridge from './bridge';
 import './clipboard';
 import './clipboard';
@@ -7,7 +7,7 @@ import './notifications';
 import './requests';
 import './requests';
 import './tabs';
 import './tabs';
 import { elemByTag } from './util-content';
 import { elemByTag } from './util-content';
-import { NS_HTML, bindEvents, createNullObj, promiseResolve } from '../util';
+import { NS_HTML, bindEvents, createNullObj, getUniqIdSafe, promiseResolve } from '../util';
 
 
 const { invokableIds, runningIds } = bridge;
 const { invokableIds, runningIds } = bridge;
 const menus = createNullObj();
 const menus = createNullObj();
@@ -22,8 +22,8 @@ let pendingSetPopup;
 
 
 // Make sure to call obj::method() in code that may run after INJECT_CONTENT userscripts
 // Make sure to call obj::method() in code that may run after INJECT_CONTENT userscripts
 (async () => {
 (async () => {
-  const contentId = getUniqId();
-  const webId = getUniqId();
+  const contentId = getUniqIdSafe();
+  const webId = getUniqIdSafe();
   // injecting right now before site scripts can mangle globals or intercept our contentId
   // injecting right now before site scripts can mangle globals or intercept our contentId
   // except for XML documents as their appearance breaks, but first we're sending
   // except for XML documents as their appearance breaks, but first we're sending
   // a request for the data because injectPageSandbox takes ~5ms
   // a request for the data because injectPageSandbox takes ~5ms
@@ -58,6 +58,7 @@ let pendingSetPopup;
     if (IS_FIREFOX) allow('InjectList', contentId);
     if (IS_FIREFOX) allow('InjectList', contentId);
     await injectScripts(contentId, webId, data, isXml);
     await injectScripts(contentId, webId, data, isXml);
   }
   }
+  allow('VaultId', contentId);
   bridge.onScripts = null;
   bridge.onScripts = null;
   sendSetPopup();
   sendSetPopup();
 })().catch(IS_FIREFOX && console.error); // Firefox can't show exceptions in content scripts
 })().catch(IS_FIREFOX && console.error); // Firefox can't show exceptions in content scripts

+ 19 - 14
src/injected/content/inject.js

@@ -1,9 +1,9 @@
 import { INJECT_CONTENT, INJECT_MAPPING, INJECT_PAGE, browser } from '#/common/consts';
 import { INJECT_CONTENT, INJECT_MAPPING, INJECT_PAGE, browser } from '#/common/consts';
-import { getUniqId, sendCmd } from '#/common';
+import { sendCmd } from '#/common';
 import { forEachKey } from '#/common/object';
 import { forEachKey } from '#/common/object';
 import bridge from './bridge';
 import bridge from './bridge';
 import { allowCommands, appendToRoot, onElement } from './util-content';
 import { allowCommands, appendToRoot, onElement } from './util-content';
-import { NS_HTML, log } from '../util';
+import { NS_HTML, getUniqIdSafe, isSameOriginWindow, log } from '../util';
 
 
 const INIT_FUNC_NAME = process.env.INIT_FUNC_NAME;
 const INIT_FUNC_NAME = process.env.INIT_FUNC_NAME;
 const VAULT_SEED_NAME = INIT_FUNC_NAME + process.env.VAULT_ID_NAME;
 const VAULT_SEED_NAME = INIT_FUNC_NAME + process.env.VAULT_ID_NAME;
@@ -13,7 +13,7 @@ let pgLists;
 let realms;
 let realms;
 /** @type boolean */
 /** @type boolean */
 let pageInjectable;
 let pageInjectable;
-let frameEventDocument;
+let frameEventWnd;
 
 
 // https://bugzil.la/1408996
 // https://bugzil.la/1408996
 let VMInitInjection = window[INIT_FUNC_NAME];
 let VMInitInjection = window[INIT_FUNC_NAME];
@@ -26,22 +26,27 @@ defineProperty(window, INIT_FUNC_NAME, {
   writable: false,
   writable: false,
 });
 });
 window::on(INIT_FUNC_NAME, evt => {
 window::on(INIT_FUNC_NAME, evt => {
-  if (!frameEventDocument) {
-    // injectPageSandbox's first event is the frame's document
-    frameEventDocument = evt::getRelatedTarget();
+  if (!frameEventWnd) {
+    // setupVaultId's first event is the frame's contentWindow
+    frameEventWnd = evt::getRelatedTarget();
   } else {
   } else {
-    // injectPageSandbox's second event is the vaultId
-    bridge.post('Frame', evt::getDetail(), INJECT_PAGE, frameEventDocument);
-    frameEventDocument = null;
+    // setupVaultId's second event is the vaultId
+    bridge.post('Frame', evt::getDetail(), INJECT_PAGE, frameEventWnd);
+    frameEventWnd = null;
   }
   }
 });
 });
 bridge.addHandlers({
 bridge.addHandlers({
   // FF bug workaround to enable processing of sourceURL in injected page scripts
   // FF bug workaround to enable processing of sourceURL in injected page scripts
   InjectList: IS_FIREFOX && injectList,
   InjectList: IS_FIREFOX && injectList,
+  /** @this {Node} window */
+  VaultId(vaultId) {
+    this[VAULT_SEED_NAME] = vaultId; // goes into the isolated world of the content scripts
+  },
 });
 });
 
 
 export function injectPageSandbox(contentId, webId) {
 export function injectPageSandbox(contentId, webId) {
-  const vaultId = !IS_TOP && setupVaultId() || '';
+  const vaultId = window[VAULT_SEED_NAME] || !IS_TOP && setupVaultId() || '';
+  delete window[VAULT_SEED_NAME];
   inject({
   inject({
     code: `(${VMInitInjection}('${vaultId}',${IS_FIREFOX}))('${webId}','${contentId}')`
     code: `(${VMInitInjection}('${vaultId}',${IS_FIREFOX}))('${webId}','${contentId}')`
       + `\n//# sourceURL=${browser.runtime.getURL('sandbox/injected-web.js')}`,
       + `\n//# sourceURL=${browser.runtime.getURL('sandbox/injected-web.js')}`,
@@ -117,6 +122,7 @@ export async function injectScripts(contentId, webId, data, isXml) {
     pgLists = null;
     pgLists = null;
     contLists = null;
     contLists = null;
   });
   });
+  VMInitInjection = null; // release for GC
 }
 }
 
 
 async function injectDelayedScripts(contentId, webId, { cache, scripts }, getReadyState) {
 async function injectDelayedScripts(contentId, webId, { cache, scripts }, getReadyState) {
@@ -221,17 +227,16 @@ function setupContentInvoker(contentId, webId) {
       : postViaBridge;
       : postViaBridge;
     fn(cmd, params, undefined, node);
     fn(cmd, params, undefined, node);
   };
   };
-  VMInitInjection = null; // release for GC
 }
 }
 
 
 function setupVaultId() {
 function setupVaultId() {
   const { parent } = window;
   const { parent } = window;
   // Testing for same-origin parent without throwing an exception.
   // Testing for same-origin parent without throwing an exception.
-  if (describeProperty(parent.location, 'href').get) {
-    const vaultId = getUniqId();
+  if (isSameOriginWindow(parent)) {
+    const vaultId = getUniqIdSafe();
     // In FF, content scripts running in a same-origin frame cannot directly call parent's functions
     // In FF, content scripts running in a same-origin frame cannot directly call parent's functions
     // TODO: Use a single PointerEvent with `pointerType: vaultId` when strict_min_version >= 59
     // TODO: Use a single PointerEvent with `pointerType: vaultId` when strict_min_version >= 59
-    parent::fire(new MouseEventSafe(INIT_FUNC_NAME, { relatedTarget: document }));
+    parent::fire(new MouseEventSafe(INIT_FUNC_NAME, { relatedTarget: window }));
     parent::fire(new CustomEventSafe(INIT_FUNC_NAME, { detail: vaultId }));
     parent::fire(new CustomEventSafe(INIT_FUNC_NAME, { detail: vaultId }));
     return vaultId;
     return vaultId;
   }
   }

+ 2 - 0
src/injected/content/safe-globals-content.js

@@ -30,7 +30,9 @@ export const {
   getOwnPropertyDescriptor: describeProperty,
   getOwnPropertyDescriptor: describeProperty,
   keys: objectKeys,
   keys: objectKeys,
 } = Object;
 } = Object;
+export const { random: mathRandom } = Math;
 export const regexpTest = RegExp[PROTO].test;
 export const regexpTest = RegExp[PROTO].test;
+export const { toStringTag } = Symbol; // used by ProtectWebpackBootstrapPlugin
 export const getDetail = describeProperty(CustomEventSafe[PROTO], 'detail').get;
 export const getDetail = describeProperty(CustomEventSafe[PROTO], 'detail').get;
 export const getRelatedTarget = describeProperty(MouseEventSafe[PROTO], 'relatedTarget').get;
 export const getRelatedTarget = describeProperty(MouseEventSafe[PROTO], 'relatedTarget').get;
 export const logging = assign({ __proto__: null }, console);
 export const logging = assign({ __proto__: null }, console);

+ 2 - 4
src/injected/content/util-content.js

@@ -58,14 +58,12 @@ export const appendToRoot = node => {
  */
  */
 export const onElement = (tag, cb, arg) => new PromiseSafe(resolve => {
 export const onElement = (tag, cb, arg) => new PromiseSafe(resolve => {
   if (elemByTag(tag)) {
   if (elemByTag(tag)) {
-    cb(arg);
-    resolve();
+    resolve(cb(arg));
   } else {
   } else {
     const observer = new MutationObserver(() => {
     const observer = new MutationObserver(() => {
       if (elemByTag(tag)) {
       if (elemByTag(tag)) {
         observer.disconnect();
         observer.disconnect();
-        cb(arg);
-        resolve();
+        resolve(cb(arg));
       }
       }
     });
     });
     // documentElement may be replaced so we'll observe the entire document
     // documentElement may be replaced so we'll observe the entire document

+ 22 - 8
src/injected/util/index.js

@@ -1,14 +1,35 @@
+const vmOwnFuncToString = () => '[Violentmonkey property]';
+
 export const NS_HTML = 'http://www.w3.org/1999/xhtml';
 export const NS_HTML = 'http://www.w3.org/1999/xhtml';
 export const CALLBACK_ID = '__CBID';
 export const CALLBACK_ID = '__CBID';
-
 /** Using __proto__ because Object.create(null) may be spoofed */
 /** Using __proto__ because Object.create(null) may be spoofed */
 export const createNullObj = () => ({ __proto__: null });
 export const createNullObj = () => ({ __proto__: null });
 export const promiseResolve = () => (async () => {})();
 export const promiseResolve = () => (async () => {})();
+export const vmOwnFunc = (func, toString) => (
+  defineProperty(func, 'toString', { value: toString || vmOwnFuncToString })
+);
 export const getOwnProp = (obj, key) => (
 export const getOwnProp = (obj, key) => (
   obj::hasOwnProperty(key)
   obj::hasOwnProperty(key)
     ? obj[key]
     ? obj[key]
     : undefined
     : undefined
 );
 );
+/** Workaround for array eavesdropping 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`. */
+export const setOwnProp = (obj, key, value) => (
+  defineProperty(obj, key, {
+    value,
+    configurable: true,
+    enumerable: true,
+    writable: true,
+  })
+);
+/** @param {Window} wnd */
+export const isSameOriginWindow = wnd => (
+  describeProperty(wnd.location, 'href').get
+);
+// Avoiding the need to safe-guard a bunch of methods so we use just one
+export const getUniqIdSafe = (prefix = 'VM') => `${prefix}${mathRandom()}`;
 
 
 export const bindEvents = (srcId, destId, bridge, cloneInto) => {
 export const bindEvents = (srcId, destId, bridge, cloneInto) => {
   /* Using a separate event for `node` because CustomEvent can't transfer nodes,
   /* Using a separate event for `node` because CustomEvent can't transfer nodes,
@@ -46,13 +67,6 @@ export const log = (level, ...args) => {
   logging[level]::apply(logging, args);
   logging[level]::apply(logging, args);
 };
 };
 
 
-/** Workaround for array eavesdropping 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`. */
-export function safePush(value) {
-  defineProperty(this, this.length, { value, writable: true, configurable: true });
-}
-
 /**
 /**
  * Picks into `this`
  * Picks into `this`
  * @param {Object} obj
  * @param {Object} obj

+ 2 - 3
src/injected/web/bridge.js

@@ -1,5 +1,4 @@
-import { getUniqId } from '#/common';
-import { CALLBACK_ID, createNullObj } from '../util';
+import { CALLBACK_ID, createNullObj, getUniqIdSafe } from '../util';
 
 
 const handlers = createNullObj();
 const handlers = createNullObj();
 const callbacks = createNullObj();
 const callbacks = createNullObj();
@@ -19,7 +18,7 @@ const bridge = {
   },
   },
   send(cmd, data, context, node) {
   send(cmd, data, context, node) {
     return new PromiseSafe(resolve => {
     return new PromiseSafe(resolve => {
-      const id = getUniqId();
+      const id = getUniqIdSafe();
       callbacks[id] = resolve;
       callbacks[id] = resolve;
       bridge.post(cmd, { [CALLBACK_ID]: id, data }, context, node);
       bridge.post(cmd, { [CALLBACK_ID]: id, data }, context, node);
     });
     });

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

@@ -1,8 +1,8 @@
 import bridge from './bridge';
 import bridge from './bridge';
 import { makeGmApi } from './gm-api';
 import { makeGmApi } from './gm-api';
 import { makeGlobalWrapper } from './gm-global-wrapper';
 import { makeGlobalWrapper } from './gm-global-wrapper';
-import { makeComponentUtils, vmOwnFunc } from './util-web';
-import { createNullObj } from '../util';
+import { makeComponentUtils } from './util-web';
+import { createNullObj, vmOwnFunc } from '../util';
 
 
 /** Name in Greasemonkey4 -> name in GM */
 /** Name in Greasemonkey4 -> name in GM */
 const GM4_ALIAS = {
 const GM4_ALIAS = {

+ 9 - 6
src/injected/web/gm-api.js

@@ -1,12 +1,15 @@
-import { dumpScriptValue, getUniqId, isEmpty } from '#/common/util';
+import { dumpScriptValue, isEmpty } from '#/common/util';
 import bridge from './bridge';
 import bridge from './bridge';
 import store from './store';
 import store from './store';
 import { onTabCreate } from './tabs';
 import { onTabCreate } from './tabs';
 import { onRequestCreate } from './requests';
 import { onRequestCreate } from './requests';
 import { onNotificationCreate } from './notifications';
 import { onNotificationCreate } from './notifications';
 import { decodeValue, dumpValue, loadValues, changeHooks } from './gm-values';
 import { decodeValue, dumpValue, loadValues, changeHooks } from './gm-values';
-import { jsonDump, vmOwnFunc } from './util-web';
-import { NS_HTML, createNullObj, promiseResolve, log, pickIntoThis } from '../util';
+import { jsonDump } from './util-web';
+import {
+  NS_HTML, createNullObj, getUniqIdSafe, log,
+  pickIntoThis, promiseResolve, vmOwnFunc,
+} from '../util';
 
 
 const {
 const {
   TextDecoder,
   TextDecoder,
@@ -62,7 +65,7 @@ export function makeGmApi() {
       const i = objectValues(hooks)::indexOf(fn);
       const i = objectValues(hooks)::indexOf(fn);
       let listenerId = i >= 0 && objectKeys(hooks)[i];
       let listenerId = i >= 0 && objectKeys(hooks)[i];
       if (!listenerId) {
       if (!listenerId) {
-        listenerId = getUniqId('VMvc');
+        listenerId = getUniqIdSafe('VMvc');
         hooks[listenerId] = fn;
         hooks[listenerId] = fn;
       }
       }
       return listenerId;
       return listenerId;
@@ -154,7 +157,7 @@ export function makeGmApi() {
      * @returns {HTMLElement} it also has .then() so it should be compatible with TM and old VM
      * @returns {HTMLElement} it also has .then() so it should be compatible with TM and old VM
      */
      */
     GM_addStyle(css) {
     GM_addStyle(css) {
-      return webAddElement(null, 'style', { textContent: css, id: getUniqId('VMst') }, this);
+      return webAddElement(null, 'style', { textContent: css, id: getUniqIdSafe('VMst') }, this);
     },
     },
     GM_openInTab(url, options) {
     GM_openInTab(url, options) {
       return onTabCreate(
       return onTabCreate(
@@ -191,7 +194,7 @@ export function makeGmApi() {
 function webAddElement(parent, tag, attrs, context) {
 function webAddElement(parent, tag, attrs, context) {
   let el;
   let el;
   let errorInfo;
   let errorInfo;
-  const cbId = getUniqId();
+  const cbId = getUniqIdSafe();
   bridge.callbacks[cbId] = function _(res) {
   bridge.callbacks[cbId] = function _(res) {
     el = this;
     el = this;
     errorInfo = res;
     errorInfo = res;

+ 6 - 6
src/injected/web/gm-global-wrapper.js

@@ -1,8 +1,8 @@
 import { isFunction } from '#/common';
 import { isFunction } from '#/common';
 import { INJECT_CONTENT } from '#/common/consts';
 import { INJECT_CONTENT } from '#/common/consts';
 import bridge from './bridge';
 import bridge from './bridge';
-import { FastLookup, vmOwnFunc } from './util-web';
-import { createNullObj, safePush } from '../util';
+import { FastLookup } from './util-web';
+import { createNullObj, setOwnProp, vmOwnFunc } from '../util';
 
 
 /** The index strings that look exactly like integers can't be forged
 /** The index strings that look exactly like integers can't be forged
  * but for example '011' doesn't look like 11 so it's allowed */
  * but for example '011' doesn't look like 11 so it's allowed */
@@ -38,7 +38,7 @@ const globalKeys = (function makeGlobalKeys() {
     && kWrappedJSObject in global
     && kWrappedJSObject in global
     && !globalKeysSet.has(kWrappedJSObject)) {
     && !globalKeysSet.has(kWrappedJSObject)) {
     globalKeysSet.add(kWrappedJSObject);
     globalKeysSet.add(kWrappedJSObject);
-    if (ok) names::safePush(kWrappedJSObject);
+    if (ok) setOwnProp(names, names.length, kWrappedJSObject);
   }
   }
   return ok ? names : globalKeysSet.toArray();
   return ok ? names : globalKeysSet.toArray();
 }());
 }());
@@ -244,9 +244,9 @@ function makeOwnKeys(local, globals) {
   const names = getOwnPropertyNames(local)::filter(notIncludedIn, globals);
   const names = getOwnPropertyNames(local)::filter(notIncludedIn, globals);
   const symbols = getOwnPropertySymbols(local)::filter(notIncludedIn, globals);
   const symbols = getOwnPropertySymbols(local)::filter(notIncludedIn, globals);
   const frameIndexes = [];
   const frameIndexes = [];
-  for (let i = 0; (global[i] || 0)::objectToString() === '[object Window]'; i += 1) {
-    if (!(i in local)) {
-      frameIndexes::safePush(`${i}`);
+  for (let i = 0, s; (global[s = `${i}`] || 0)::objectToString() === '[object Window]'; i += 1) {
+    if (!(s in local)) {
+      setOwnProp(frameIndexes, s, s);
     }
     }
   }
   }
   return []::concat(
   return []::concat(

+ 15 - 3
src/injected/web/index.js

@@ -1,12 +1,15 @@
 import { INJECT_PAGE, INJECT_CONTENT } from '#/common/consts';
 import { INJECT_PAGE, INJECT_CONTENT } from '#/common/consts';
-import { bindEvents, createNullObj, log } from '../util';
 import bridge from './bridge';
 import bridge from './bridge';
 import store from './store';
 import store from './store';
+import { makeGmApiWrapper } from './gm-api-wrapper';
 import './gm-values';
 import './gm-values';
 import './notifications';
 import './notifications';
 import './requests';
 import './requests';
 import './tabs';
 import './tabs';
-import { makeGmApiWrapper } from './gm-api-wrapper';
+import {
+  bindEvents, createNullObj, getUniqIdSafe,
+  isSameOriginWindow, log, setOwnProp, vmOwnFunc,
+} from '../util';
 
 
 // Make sure to call safe::methods() in code that may run after userscripts
 // Make sure to call safe::methods() in code that may run after userscripts
 
 
@@ -36,7 +39,7 @@ export default function initialize(
     bridge.mode = INJECT_PAGE;
     bridge.mode = INJECT_PAGE;
     bindEvents(webId, contentId, bridge);
     bindEvents(webId, contentId, bridge);
     bridge.addHandlers({
     bridge.addHandlers({
-      /** @this {Node} contentDocument */
+      /** @this {Node} contentWindow */
       Frame(id) {
       Frame(id) {
         this[id] = VAULT;
         this[id] = VAULT;
       },
       },
@@ -44,6 +47,15 @@ export default function initialize(
         bridge.post('Pong');
         bridge.post('Pong');
       },
       },
     });
     });
+    setOwnProp(window, 'open', vmOwnFunc(function open(...args) {
+      const wnd = openWindow::apply(this, args);
+      const vaultId = wnd && isSameOriginWindow(wnd) && getUniqIdSafe();
+      if (vaultId) {
+        wnd[vaultId] = VAULT;
+        bridge.post('VaultId', vaultId, undefined, wnd);
+      }
+      return wnd;
+    }, funcToString::bind(openWindow)));
   }
   }
   return invokeGuest;
   return invokeGuest;
 }
 }

+ 3 - 3
src/injected/web/requests.js

@@ -1,5 +1,5 @@
-import { getUniqId, isFunction } from '#/common';
-import { NS_HTML, createNullObj, getOwnProp, log, pickIntoThis } from '../util';
+import { isFunction } from '#/common';
+import { NS_HTML, createNullObj, getOwnProp, getUniqIdSafe, log, pickIntoThis } from '../util';
 import bridge from './bridge';
 import bridge from './bridge';
 
 
 const idMap = createNullObj();
 const idMap = createNullObj();
@@ -19,7 +19,7 @@ bridge.addHandlers({
 export function onRequestCreate(opts, context) {
 export function onRequestCreate(opts, context) {
   if (!opts.url) throw new ErrorSafe('Required parameter "url" is missing.');
   if (!opts.url) throw new ErrorSafe('Required parameter "url" is missing.');
   const scriptId = context.id;
   const scriptId = context.id;
-  const id = getUniqId(`VMxhr${scriptId}`);
+  const id = getUniqIdSafe(`VMxhr${scriptId}`);
   const req = {
   const req = {
     __proto__: null,
     __proto__: null,
     id,
     id,

+ 9 - 8
src/injected/web/safe-globals-web.js

@@ -22,6 +22,7 @@ export let
   fire,
   fire,
   off,
   off,
   on,
   on,
+  openWindow,
   // Symbol
   // Symbol
   scopeSym,
   scopeSym,
   toStringTag,
   toStringTag,
@@ -53,16 +54,13 @@ export let
   charCodeAt,
   charCodeAt,
   slice,
   slice,
   replace,
   replace,
-  // Set.prototype
-  setDelete,
-  /** @type {Set.prototype.forEach} */
-  setForEach,
-  setHas,
   // document
   // document
   createElementNS,
   createElementNS,
   // various methods
   // various methods
   safeCall,
   safeCall,
+  funcToString,
   jsonParse,
   jsonParse,
+  mathRandom,
   regexpTest,
   regexpTest,
   then,
   then,
   logging,
   logging,
@@ -83,8 +81,8 @@ export const VAULT = (() => {
   let i = -1;
   let i = -1;
   let res;
   let res;
   if (process.env.VAULT_ID) {
   if (process.env.VAULT_ID) {
-    res = document[process.env.VAULT_ID];
-    delete document[process.env.VAULT_ID];
+    res = window[process.env.VAULT_ID];
+    delete window[process.env.VAULT_ID];
   }
   }
   if (!res) {
   if (!res) {
     res = { __proto__: null };
     res = { __proto__: null };
@@ -96,7 +94,7 @@ export const VAULT = (() => {
     ErrorSafe = res[i += 1] || window.Error,
     ErrorSafe = res[i += 1] || window.Error,
     KeyboardEventSafe = res[i += 1] || window.KeyboardEvent,
     KeyboardEventSafe = res[i += 1] || window.KeyboardEvent,
     MouseEventSafe = res[i += 1] || window.MouseEvent,
     MouseEventSafe = res[i += 1] || window.MouseEvent,
-    Object = res[i += 1] || window.Object, // minification and guarding webpack Object(import) calls
+    Object = res[i += 1] || window.Object,
     PromiseSafe = res[i += 1] || window.Promise,
     PromiseSafe = res[i += 1] || window.Promise,
     ProxySafe = res[i += 1] || global.Proxy, // In FF content mode it's not equal to window.Proxy
     ProxySafe = res[i += 1] || global.Proxy, // In FF content mode it's not equal to window.Proxy
     Uint8ArraySafe = res[i += 1] || window.Uint8Array,
     Uint8ArraySafe = res[i += 1] || window.Uint8Array,
@@ -104,6 +102,7 @@ export const VAULT = (() => {
     fire = res[i += 1] || window.dispatchEvent,
     fire = res[i += 1] || window.dispatchEvent,
     off = res[i += 1] || window.removeEventListener,
     off = res[i += 1] || window.removeEventListener,
     on = res[i += 1] || window.addEventListener,
     on = res[i += 1] || window.addEventListener,
+    openWindow = res[i += 1] || window.open,
     // Symbol
     // Symbol
     scopeSym = res[i += 1] || Symbol.unscopables,
     scopeSym = res[i += 1] || Symbol.unscopables,
     toStringTag = res[i += 1] || Symbol.toStringTag,
     toStringTag = res[i += 1] || Symbol.toStringTag,
@@ -137,7 +136,9 @@ export const VAULT = (() => {
     createElementNS = res[i += 1] || document.createElementNS,
     createElementNS = res[i += 1] || document.createElementNS,
     // various methods
     // various methods
     safeCall = res[i += 1] || Object.call.bind(Object.call),
     safeCall = res[i += 1] || Object.call.bind(Object.call),
+    funcToString = res[i += 1] || safeCall.toString,
     jsonParse = res[i += 1] || JSON.parse,
     jsonParse = res[i += 1] || JSON.parse,
+    mathRandom = res[i += 1] || Math.random,
     regexpTest = res[i += 1] || RegExp[PROTO].test,
     regexpTest = res[i += 1] || RegExp[PROTO].test,
     then = res[i += 1] || PromiseSafe[PROTO].then,
     then = res[i += 1] || PromiseSafe[PROTO].then,
     logging = res[i += 1] || assign({ __proto__: null }, console),
     logging = res[i += 1] || assign({ __proto__: null }, console),

+ 0 - 6
src/injected/web/util-web.js

@@ -2,7 +2,6 @@ import { createNullObj } from '../util';
 import bridge from '#/injected/web/bridge';
 import bridge from '#/injected/web/bridge';
 import { INJECT_CONTENT } from '#/common/consts';
 import { INJECT_CONTENT } from '#/common/consts';
 
 
-const vmOwnFuncToString = () => '[Violentmonkey property]';
 // Firefox defines `isFinite` on `global` not on `window`
 // Firefox defines `isFinite` on `global` not on `window`
 const { isFinite } = global; // eslint-disable-line no-restricted-properties
 const { isFinite } = global; // eslint-disable-line no-restricted-properties
 const { toString: numberToString } = 0;
 const { toString: numberToString } = 0;
@@ -100,11 +99,6 @@ export const FastLookup = (hubs = createNullObj()) => {
   }
   }
 };
 };
 
 
-export const vmOwnFunc = (func, toString) => {
-  defineProperty(func, 'toString', { value: toString || vmOwnFuncToString });
-  return func;
-};
-
 /**
 /**
  * Adding the polyfills in Chrome (always as it doesn't provide them)
  * Adding the polyfills in Chrome (always as it doesn't provide them)
  * and in Firefox page mode (while preserving the native ones in content mode)
  * and in Firefox page mode (while preserving the native ones in content mode)