Przeglądaj źródła

fix: check web->content permissions

tophf 4 lat temu
rodzic
commit
6dac93292b

+ 8 - 2
src/background/utils/db.js

@@ -42,6 +42,12 @@ Object.assign(commands, {
   GetScriptCode(id) {
     return storage.code.getOne(id);
   },
+  GetScriptVer(opts) {
+    const script = getScript(opts);
+    return script && !script.config.removed
+      ? script.meta.version
+      : null;
+  },
   /** @return {Promise<void>} */
   MarkRemoved({ id, removed }) {
     return updateScriptInfo(id, {
@@ -139,7 +145,7 @@ preInitialize.push(async () => {
       // listing all known resource urls in order to remove unused mod keys
       const {
         custom: { pathMap = {} } = {},
-        meta = {},
+        meta = script.meta = {},
       } = script;
       meta.grant = [...new Set(meta.grant || [])]; // deduplicate
       meta.require?.forEach(rememberUrl, pathMap);
@@ -290,7 +296,7 @@ export async function getScriptsByURL(url, isTop) {
     const runAt = `${custom.runAt || meta.runAt || ''}`.match(RUN_AT_RE)?.[1] || 'end';
     const env = runAt === 'start' || runAt === 'body' ? envStart : envDelayed;
     env.ids.push(id);
-    if (meta.grant?.some(GMVALUES_RE.test, GMVALUES_RE)) {
+    if (meta.grant.some(GMVALUES_RE.test, GMVALUES_RE)) {
       env[ENV_VALUE_IDS].push(id);
     }
     for (const [list, name] of [

+ 26 - 10
src/injected/content/bridge.js

@@ -1,10 +1,18 @@
 import { sendCmd } from '#/common';
 import { INJECT_PAGE, browser } from '#/common/consts';
 
-// {CommandName: sendCmd} will relay the request via sendCmd as is
+const allow = createNullObj();
 /** @type {Object.<string, MessageFromGuestHandler>} */
 const handlers = createNullObj();
 const bgHandlers = createNullObj();
+const onScripts = [];
+const assignHandlers = (dest, src, force) => {
+  if (force) {
+    assign(dest, src);
+  } else {
+    onScripts.push(() => assign(dest, src));
+  }
+};
 const bridge = {
   __proto__: null, // Object.create(null) may be spoofed
   ids: [], // all ids including the disabled ones for SetPopup
@@ -13,21 +21,29 @@ const bridge = {
   /** @type Number[] */
   invokableIds: [],
   failedIds: [],
-  // {CommandName: sendCmd} will relay the request via sendCmd as is
-  addHandlers(obj) {
-    assign(handlers, obj);
+  onScripts,
+  /** Without `force` handlers will be added only when userscripts are about to be injected. */
+  addHandlers(obj, force) {
+    assignHandlers(handlers, obj, force);
+  },
+  /** { CommandName: true } will relay the request via sendCmd as is.
+   * Without `force` handlers will be added only when userscripts are about to be injected. */
+  addBackgroundHandlers(obj, force) {
+    assignHandlers(bgHandlers, obj, force);
   },
-  addBackgroundHandlers(obj) {
-    assign(bgHandlers, obj);
+  allow(cmd, dataKey) {
+    (allow[cmd] || (allow[cmd] = createNullObj()))[dataKey] = true;
   },
   // realm is provided when called directly via invokeHost
-  async onHandle({ cmd, data }, realm) {
+  async onHandle({ cmd, data, dataKey }, realm) {
     const handle = handlers[cmd];
-    if (!handle) throw new Error(`Invalid command: ${cmd}`);
+    if (!handle || !allow[cmd]?.[dataKey]) {
+      throw new Error(`[Violentmonkey] Invalid command: "${cmd}" on ${global.location.host}`);
+    }
     const callbackId = data?.callbackId;
     const payload = callbackId ? data.payload : data;
-    let res = handle === sendCmd ? sendCmd(cmd, payload) : handle(payload, realm || INJECT_PAGE);
-    if (typeof res?.then === 'function') {
+    let res = handle === true ? sendCmd(cmd, payload) : handle(payload, realm || INJECT_PAGE);
+    if (res instanceof Promise) {
       res = await res;
     }
     if (callbackId && res !== undefined) {

+ 29 - 40
src/injected/content/clipboard.js

@@ -1,43 +1,32 @@
-import { sendCmd } from '#/common';
-import { logging } from '../utils/helpers';
+import { log } from '../utils/helpers';
 import bridge from './bridge';
 
-// old Firefox defines it on a different prototype so we'll just grab it from document directly
-const { execCommand } = document;
-const { setData } = DataTransfer[Prototype];
-const { get: getClipboardData } = describeProperty(ClipboardEvent[Prototype], 'clipboardData');
-const { preventDefault, stopImmediatePropagation } = Event[Prototype];
-
-let clipboardData;
-
-bridge.addHandlers({
-  __proto__: null, // Object.create(null) may be spoofed
-  SetClipboard(data) {
-    if (bridge.isFirefox) {
-      // Firefox does not support copy from background page.
-      // ref: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard
-      // The dirty way will create a <textarea> element in web page and change the selection.
-      setClipboard(data);
-    } else {
-      sendCmd('SetClipboard', data);
-    }
-  },
-});
-
-function onCopy(e) {
-  e::stopImmediatePropagation();
-  e::preventDefault();
-  const { type, data } = clipboardData;
-  e::getClipboardData()::setData(type || 'text/plain', data);
-}
-
-function setClipboard({ type, data }) {
-  clipboardData = { type, data };
-  document::addEventListener('copy', onCopy, false);
-  const ret = document::execCommand('copy', false, null);
-  document::removeEventListener('copy', onCopy, false);
-  clipboardData = null;
-  if (process.env.DEBUG && !ret) {
-    logging.warn('Copy failed!');
+bridge.onScripts.push(() => {
+  let setClipboard;
+  if (bridge.isFirefox) {
+    let clipboardData;
+    // old Firefox defines it on a different prototype so we'll just grab it from document directly
+    const { execCommand } = document;
+    const { setData } = DataTransfer[Prototype];
+    const { get: getClipboardData } = describeProperty(ClipboardEvent[Prototype], 'clipboardData');
+    const { preventDefault, stopImmediatePropagation } = Event[Prototype];
+    const onCopy = e => {
+      e::stopImmediatePropagation();
+      e::preventDefault();
+      e::getClipboardData()::setData(clipboardData.type || 'text/plain', clipboardData.data);
+    };
+    setClipboard = params => {
+      clipboardData = params;
+      document::addEventListener('copy', onCopy);
+      if (!document::execCommand('copy') && process.env.DEBUG) {
+        log('warn', null, 'GM_setClipboard failed!');
+      }
+      document::removeEventListener('copy', onCopy);
+      clipboardData = null;
+    };
   }
-}
+  bridge.addHandlers({
+    __proto__: null, // Object.create(null) may be spoofed
+    SetClipboard: setClipboard || true,
+  }, true);
+});

+ 27 - 11
src/injected/content/index.js

@@ -37,18 +37,39 @@ const { split } = '';
   const data = IS_FIREFOX && Event[Prototype].composedPath
     ? await getDataFF(dataPromise)
     : await dataPromise;
+  const { allow } = bridge;
   // 1) bridge.post may be overridden in injectScripts
   // 2) cloneInto is provided by Firefox in content scripts to expose data to the page
-  bridge.post = bindEvents(contentId, webId, bridge.onHandle, global.cloneInto);
+  bindEvents(contentId, webId, bridge, global.cloneInto);
+  bridge.contentId = contentId;
   bridge.ids = data.ids;
   bridge.isFirefox = data.info.isFirefox;
   bridge.injectInto = data.injectInto;
   isPopupShown = data.isPopupShown;
-  if (data.expose) bridge.post('Expose');
-  if (data.scripts) await injectScripts(contentId, webId, data, isXml);
+  if (data.expose) {
+    allow('GetScriptVer', contentId);
+    bridge.addHandlers({ GetScriptVer: true }, true);
+    bridge.post('Expose');
+  }
+  if (data.scripts) {
+    bridge.onScripts.forEach(fn => fn());
+    allow('SetTimeout', contentId);
+    allow('InjectList', contentId);
+    allow('Pong', contentId);
+    await injectScripts(contentId, webId, data, isXml);
+  }
+  bridge.onScripts = null;
   sendSetPopup();
 })().catch(IS_FIREFOX && console.error); // Firefox can't show exceptions in content scripts
 
+bridge.addBackgroundHandlers({
+  __proto__: null, // Object.create(null) may be spoofed
+  PopupShown(state) {
+    isPopupShown = state;
+    sendSetPopup();
+  },
+}, true);
+
 bridge.addBackgroundHandlers({
   __proto__: null, // Object.create(null) may be spoofed
   Command(data) {
@@ -56,10 +77,6 @@ bridge.addBackgroundHandlers({
     const realm = invokableIds::includes(id) && INJECT_CONTENT;
     bridge.post('Command', data, realm);
   },
-  PopupShown(state) {
-    isPopupShown = state;
-    sendSetPopup();
-  },
   UpdatedValues(data) {
     const dataPage = createNullObj();
     const dataContent = createNullObj();
@@ -73,7 +90,6 @@ bridge.addBackgroundHandlers({
 
 bridge.addHandlers({
   __proto__: null, // Object.create(null) may be spoofed
-  UpdateValue: sendCmd,
   RegisterMenu(data) {
     if (IS_TOP) {
       const id = data[0];
@@ -108,9 +124,9 @@ bridge.addHandlers({
       return e.stack;
     }
   },
-  GetScript: sendCmd,
-  SetTimeout: sendCmd,
-  TabFocus: sendCmd,
+  SetTimeout: true,
+  TabFocus: true,
+  UpdateValue: true,
 });
 
 async function sendSetPopup(isDelayed) {

+ 42 - 3
src/injected/content/inject.js

@@ -5,14 +5,15 @@ import { elemByTag, NS_HTML, log } from '../utils/helpers';
 import bridge from './bridge';
 
 // Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1408996
-const VMInitInjection = window[process.env.INIT_FUNC_NAME];
+let VMInitInjection = window[process.env.INIT_FUNC_NAME];
 // To avoid running repeatedly due to new `document.documentElement`
 // (the prop is undeletable so a userscript can't fool us on reinjection)
 defineProperty(window, process.env.INIT_FUNC_NAME, { value: 1 });
 
+const regexpTest = RegExp[Prototype].test;
 const stringIncludes = ''.includes;
 const resolvedPromise = Promise.resolve();
-const { runningIds } = bridge;
+const { allow, runningIds } = bridge;
 let contLists;
 let pgLists;
 /** @type {Object<string,VMInjectionRealm>} */
@@ -108,6 +109,7 @@ export async function injectScripts(contentId, webId, data, isXml) {
       const realmData = realms[realm];
       realmData.lists[script.runAt].push(script); // 'start' or 'body' per getScriptsByURL()
       realmData.is = true;
+      allowCommands(script);
     } else {
       bridge.failedIds.push(id);
     }
@@ -168,6 +170,7 @@ async function injectDelayedScripts(contentId, webId, { cache, scripts }, getRea
   if (needsInvoker && contentId) {
     setupContentInvoker(contentId, webId);
   }
+  scripts::forEach(allowCommands);
   injectAll('end');
   injectAll('idle');
 }
@@ -177,7 +180,7 @@ function checkInjectable() {
     Pong() {
       pageInjectable = true;
     },
-  });
+  }, true);
   bridge.post('Ping');
   return pageInjectable;
 }
@@ -264,4 +267,40 @@ function setupContentInvoker(contentId, webId) {
   bridge.post = (cmd, params, realm) => (
     (realm === INJECT_CONTENT ? invokeContent : postViaBridge)(cmd, params)
   );
+  VMInitInjection = null; // release for GC
+}
+
+/**
+ * @param {VMInjectedScript | VMScript} script
+ */
+function allowCommands(script) {
+  const { dataKey } = script;
+  allow('Run', dataKey);
+  script.meta.grant::forEach(grant => {
+    const gm = /^GM[._]/::regexpTest(grant) && grant::slice(3);
+    if (grant === 'GM_xmlhttpRequest' || grant === 'GM.xmlHttpRequest' || gm === 'download') {
+      allow('AbortRequest', dataKey);
+      allow('HttpRequest', dataKey);
+    } else if (grant === 'window.close') {
+      allow('TabClose', dataKey);
+    } else if (grant === 'window.focus') {
+      allow('TabFocus', dataKey);
+    } else if (gm === 'addElement' || gm === 'addStyle') {
+      allow('AddElement', dataKey);
+    } else if (gm === 'setValue' || gm === 'deleteValue') {
+      allow('UpdateValue', dataKey);
+    } else if (gm === 'notification') {
+      allow('Notification', dataKey);
+      allow('RemoveNotification', dataKey);
+    } else if (gm === 'openInTab') {
+      allow('TabOpen', dataKey);
+      allow('TabClose', dataKey);
+    } else if (gm === 'registerMenuCommand') {
+      allow('RegisterMenu', dataKey);
+    } else if (gm === 'setClipboard') {
+      allow('SetClipboard', dataKey);
+    } else if (gm === 'unregisterMenuCommand') {
+      allow('UnregisterMenu', dataKey);
+    }
+  });
 }

+ 1 - 1
src/injected/content/requests.js

@@ -16,7 +16,7 @@ bridge.addHandlers({
     };
     sendCmd('HttpRequest', opts);
   },
-  AbortRequest: sendCmd,
+  AbortRequest: true,
 });
 
 bridge.addBackgroundHandlers({

+ 8 - 8
src/injected/utils/index.js

@@ -1,11 +1,11 @@
-export function bindEvents(srcId, destId, handle, cloneInto) {
-  const getDetail = describeProperty(CustomEvent[Prototype], 'detail').get;
-  const pageContext = cloneInto && document.defaultView;
-  document::addEventListener(srcId, e => handle(e::getDetail()));
-  return (cmd, params) => {
-    const data = { cmd, data: params };
-    const detail = cloneInto ? cloneInto(data, pageContext) : data;
+const getDetail = describeProperty(CustomEvent[Prototype], 'detail').get;
+
+export function bindEvents(srcId, destId, bridge, cloneInto) {
+  global::addEventListener(srcId, e => bridge.onHandle(e::getDetail()));
+  bridge.post = (cmd, params, context) => {
+    const data = { cmd, data: params, dataKey: (context || bridge).dataKey };
+    const detail = cloneInto ? cloneInto(data, document) : data;
     const e = new CustomEvent(destId, { detail });
-    document::dispatchEvent(e);
+    global::dispatchEvent(e);
   };
 }

+ 6 - 6
src/injected/web/bridge.js

@@ -13,25 +13,25 @@ const bridge = {
     const fn = handlers[cmd];
     if (fn) fn(data);
   },
-  send(cmd, data) {
+  send(cmd, data, context) {
     return new Promise(resolve => {
-      postWithCallback(cmd, data, resolve);
+      postWithCallback(cmd, data, context, resolve);
     });
   },
-  sendSync(cmd, data) {
+  sendSync(cmd, data, context) {
     let res;
-    postWithCallback(cmd, data, payload => { res = payload; });
+    postWithCallback(cmd, data, context, payload => { res = payload; });
     return res;
   },
 };
 
-function postWithCallback(cmd, data, cb) {
+function postWithCallback(cmd, data, context, cb) {
   const id = getUniqId();
   callbacks[id] = (payload) => {
     delete callbacks[id];
     cb(payload);
   };
-  bridge.post(cmd, { callbackId: id, payload: data });
+  bridge.post(cmd, { callbackId: id, payload: data }, context);
 }
 
 export default bridge;

+ 27 - 24
src/injected/web/gm-api.js

@@ -34,7 +34,7 @@ export function makeGmApi() {
       const oldRaw = values[key];
       delete values[key];
       // using `undefined` to match the documentation and TM for GM_addValueChangeListener
-      dumpValue(id, key, undefined, null, oldRaw);
+      dumpValue(id, key, undefined, null, oldRaw, this);
     },
     GM_getValue(key, def) {
       const raw = loadValues(this.id)[key];
@@ -49,7 +49,7 @@ export function makeGmApi() {
       const values = loadValues(id);
       const oldRaw = values[key];
       values[key] = raw;
-      dumpValue(id, key, val, raw, oldRaw);
+      dumpValue(id, key, val, raw, oldRaw, this);
     },
     /**
      * @callback GMValueChangeListener
@@ -103,14 +103,14 @@ export function makeGmApi() {
       const { id } = this;
       const key = `${id}:${cap}`;
       store.commands[key] = func;
-      bridge.post('RegisterMenu', [id, cap]);
+      bridge.post('RegisterMenu', [id, cap], this);
       return cap;
     },
     GM_unregisterMenuCommand(cap) {
       const { id } = this;
       const key = `${id}:${cap}`;
       delete store.commands[key];
-      bridge.post('UnregisterMenu', [id, cap]);
+      bridge.post('UnregisterMenu', [id, cap], this);
     },
     GM_download(arg1, name) {
       // not using ... as it calls Babel's polyfill that calls unsafe Object.xxx
@@ -140,10 +140,10 @@ export function makeGmApi() {
         overrideMimeType: 'application/octet-stream',
         onload: downloadBlob,
       });
-      return onRequestCreate(opts, this.id);
+      return onRequestCreate(opts, this);
     },
     GM_xmlhttpRequest(opts) {
-      return onRequestCreate(opts, this.id);
+      return onRequestCreate(opts, this);
     },
     /**
      * Bypasses site's CSP for inline `style`, `link`, and `script`.
@@ -152,24 +152,27 @@ export function makeGmApi() {
      * @param {Object} [attributes]
      * @returns {HTMLElement} it also has .then() so it should be compatible with TM
      */
-    GM_addElement: (parent, tag, attributes) => (
-      typeof parent === 'string'
-        ? webAddElement(undefined, parent, tag)
-        : webAddElement(parent, tag, attributes)
-    ),
+    GM_addElement(parent, tag, attributes) {
+      return typeof parent === 'string'
+        ? webAddElement(null, parent, tag, this)
+        : webAddElement(parent, tag, attributes, this);
+    },
     /**
      * Bypasses site's CSP for inline `style`.
      * @param {string} css
      * @returns {HTMLElement} it also has .then() so it should be compatible with TM and old VM
      */
-    GM_addStyle: css => (
-      webAddElement(undefined, 'style', { textContent: css }, getUniqId('VMst'))
-    ),
-    GM_openInTab: (url, options) => (
-      onTabCreate(options && typeof options === 'object'
-        ? assign({}, options, { url })
-        : { active: !options, url })
-    ),
+    GM_addStyle(css) {
+      return webAddElement(null, 'style', { textContent: css }, this, getUniqId('VMst'));
+    },
+    GM_openInTab(url, options) {
+      return onTabCreate(
+        options && typeof options === 'object'
+          ? assign({}, options, { url })
+          : { active: !options, url },
+        this,
+      );
+    },
     GM_notification(text, title, image, onclick) {
       const options = typeof text === 'object' ? text : {
         text,
@@ -180,24 +183,24 @@ export function makeGmApi() {
       if (!options.text) {
         throw new Error('GM_notification: `text` is required!');
       }
-      const id = onNotificationCreate(options);
+      const id = onNotificationCreate(options, this);
       return {
-        remove: vmOwnFunc(() => bridge.send('RemoveNotification', id)),
+        remove: vmOwnFunc(() => bridge.send('RemoveNotification', id, this)),
       };
     },
     GM_setClipboard(data, type) {
-      bridge.post('SetClipboard', { data, type });
+      bridge.post('SetClipboard', { data, type }, this);
     },
     // using the native console.log so the output has a clickable link to the caller's source
     GM_log: logging.log,
   };
 }
 
-function webAddElement(parent, tag, attributes, useId) {
+function webAddElement(parent, tag, attributes, context, useId) {
   const id = useId || getUniqId('VMel');
   let el;
   // DOM error in content script can't be caught by a page-mode userscript so we rethrow it here
-  let error = bridge.sendSync('AddElement', { tag, attributes, id });
+  let error = bridge.sendSync('AddElement', { tag, attributes, id }, context);
   if (!error) {
     try {
       el = document::getElementById(id);

+ 2 - 2
src/injected/web/gm-values.js

@@ -36,8 +36,8 @@ export function loadValues(id) {
   return store.values[id];
 }
 
-export function dumpValue(id, key, val, raw, oldRaw) {
-  bridge.post('UpdateValue', { id, key, value: raw });
+export function dumpValue(id, key, val, raw, oldRaw, context) {
+  bridge.post('UpdateValue', { id, key, value: raw }, context);
   if (raw !== oldRaw) {
     const hooks = changeHooks[id]?.[key];
     if (hooks) notifyChange(hooks, key, val, raw, oldRaw);

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

@@ -42,6 +42,7 @@ export function wrapGM(script) {
     id,
     script,
     resources,
+    dataKey: script.dataKey,
     pathMap: script.custom.pathMap || createNullObj(),
     urls: createNullObj(),
   };
@@ -61,10 +62,10 @@ export function wrapGM(script) {
   // not using ...spread as it calls Babel's polyfill that calls unsafe Object.xxx
   assign(gm, componentUtils);
   if (grant::includes('window.close')) {
-    gm.close = vmOwnFunc(() => bridge.post('TabClose'));
+    gm.close = vmOwnFunc(() => bridge.post('TabClose', 0, context));
   }
   if (grant::includes('window.focus')) {
-    gm.focus = vmOwnFunc(() => bridge.post('TabFocus'));
+    gm.focus = vmOwnFunc(() => bridge.post('TabFocus', 0, context));
   }
   if (!gmApi && grant.length) gmApi = makeGmApi();
   grant::forEach((name) => {

+ 9 - 7
src/injected/web/index.js

@@ -24,9 +24,12 @@ export default function initialize(
   invokeHost,
 ) {
   let invokeGuest;
+  bridge.dataKey = contentId;
   if (invokeHost) {
     bridge.mode = INJECT_CONTENT;
-    bridge.post = (cmd, data) => invokeHost({ cmd, data }, INJECT_CONTENT);
+    bridge.post = (cmd, data, context) => {
+      invokeHost({ cmd, data, dataKey: (context || bridge).dataKey }, INJECT_CONTENT);
+    };
     invokeGuest = (cmd, data) => bridge.onHandle({ cmd, data });
     global.chrome = undefined;
     global.browser = undefined;
@@ -35,7 +38,7 @@ export default function initialize(
     });
   } else {
     bridge.mode = INJECT_PAGE;
-    bridge.post = bindEvents(webId, contentId, bridge.onHandle);
+    bindEvents(webId, contentId, bridge);
     bridge.addHandlers({
       Ping() {
         bridge.post('Pong');
@@ -76,10 +79,9 @@ bridge.addHandlers({
   Expose() {
     window.external.Violentmonkey = {
       version: process.env.VM_VER,
-      async isInstalled(name, namespace) {
-        const script = await bridge.send('GetScript', { meta: { name, namespace } });
-        return script && !script.config.removed ? script.meta.version : null;
-      },
+      isInstalled: (name, namespace) => (
+        bridge.send('GetScriptVer', { meta: { name, namespace } })
+      ),
     };
   },
 });
@@ -105,8 +107,8 @@ async function onCodeSet(item, fn) {
     log('info', [bridge.mode], item.displayName);
   }
   const run = () => {
+    bridge.post('Run', item.props.id, item);
     wrapGM(item)::fn(logging.error);
-    bridge.post('Run', item.props.id);
   };
   const el = document::getCurrentScript();
   const wait = waiters[stage];

+ 2 - 2
src/injected/web/notifications.js

@@ -19,7 +19,7 @@ bridge.addHandlers({
   },
 });
 
-export function onNotificationCreate(options) {
+export function onNotificationCreate(options, context) {
   lastId += 1;
   notifications[lastId] = options;
   bridge.post('Notification', {
@@ -27,6 +27,6 @@ export function onNotificationCreate(options) {
     text: options.text,
     title: options.title,
     image: options.image,
-  });
+  }, context);
   return lastId;
 }

+ 6 - 5
src/injected/web/requests.js

@@ -20,18 +20,19 @@ bridge.addHandlers({
   },
 });
 
-export function onRequestCreate(opts, scriptId) {
+export function onRequestCreate(opts, context) {
   if (!opts.url) throw new Error('Required parameter "url" is missing.');
+  const scriptId = context.id;
   const id = getUniqId(`VMxhr${scriptId}`);
   const req = {
     id,
     scriptId,
     opts,
   };
-  start(req);
+  start(req, context);
   return {
     abort() {
-      bridge.post('AbortRequest', id);
+      bridge.post('AbortRequest', id, context);
     },
   };
 }
@@ -138,7 +139,7 @@ function receiveChunk(req, { data, i, last }) {
   }
 }
 
-async function start(req) {
+async function start(req, context) {
   const { id, opts, scriptId } = req;
   // withCredentials is for GM4 compatibility and used only if `anonymous` is not set,
   // it's true by default per the standard/historical behavior of gmxhr
@@ -175,7 +176,7 @@ async function start(req) {
     'password',
     'timeout',
     'user',
-  ])));
+  ])), context);
 }
 
 function getFullUrl(url) {

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

@@ -16,17 +16,17 @@ bridge.addHandlers({
   },
 });
 
-export function onTabCreate(data) {
+export function onTabCreate(data, context) {
   lastId += 1;
   const key = lastId;
   const item = {
     onclose: null,
     closed: false,
     close() {
-      bridge.post('TabClose', key);
+      bridge.post('TabClose', key, context);
     },
   };
   tabs[key] = item;
-  bridge.post('TabOpen', { key, data });
+  bridge.post('TabOpen', { key, data }, context);
   return item;
 }