Browse Source

fix: don't expose contentId/webId in DOM

tophf 4 years ago
parent
commit
fdc1f7b971

+ 4 - 1
scripts/webpack.conf.js

@@ -20,6 +20,7 @@ const INIT_FUNC_NAME = `Violentmonkey:${
   ).toString('base64')
   ).toString('base64')
 }`;
 }`;
 const VAULT_ID = '__VAULT_ID__';
 const VAULT_ID = '__VAULT_ID__';
+const HANDSHAKE_ID = '__HANDSHAKE_ID__';
 // eslint-disable-next-line import/no-dynamic-require
 // eslint-disable-next-line import/no-dynamic-require
 const VM_VER = require(`${defaultOptions.distDir}/manifest.json`).version;
 const VM_VER = require(`${defaultOptions.distDir}/manifest.json`).version;
 const WEBPACK_OPTS = {
 const WEBPACK_OPTS = {
@@ -84,6 +85,8 @@ const defsObj = {
   '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_NAME': JSON.stringify(VAULT_ID),
   'process.env.VAULT_ID': VAULT_ID,
   'process.env.VAULT_ID': VAULT_ID,
+  'process.env.HANDSHAKE_ID': HANDSHAKE_ID,
+  'process.env.HANDSHAKE_ACK': '1',
 };
 };
 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');
 const definitions = new webpack.DefinePlugin(defsObj);
 const definitions = new webpack.DefinePlugin(defsObj);
@@ -190,7 +193,7 @@ module.exports = Promise.all([
     config.plugins.push(new ProtectWebpackBootstrapPlugin());
     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 (IS_FIREFOX,${HANDSHAKE_ID},${VAULT_ID}) {
           const module = { __proto__: null };
           const module = { __proto__: null };
           ${getGlobals()}`,
           ${getGlobals()}`,
       footer: `
       footer: `

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

@@ -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, getUniqIdSafe, promiseResolve } from '../util';
+import { NS_HTML, createNullObj, getUniqIdSafe, promiseResolve } from '../util';
 
 
 const { invokableIds, runningIds } = bridge;
 const { invokableIds, runningIds } = bridge;
 const menus = createNullObj();
 const menus = createNullObj();
@@ -33,8 +33,6 @@ let pendingSetPopup;
     IS_FIREFOX && global.location.href,
     IS_FIREFOX && global.location.href,
     { retry: true });
     { retry: true });
   const isXml = document instanceof XMLDocument;
   const isXml = document instanceof XMLDocument;
-  // Binding now so injectPageSandbox can call our bridge.post before `data` is received
-  bindEvents(contentId, webId, bridge, global.cloneInto);
   if (!isXml) injectPageSandbox(contentId, webId);
   if (!isXml) injectPageSandbox(contentId, webId);
   // detecting if browser.contentScripts is usable, it was added in FF59 as well as composedPath
   // detecting if browser.contentScripts is usable, it was added in FF59 as well as composedPath
   const data = IS_FIREFOX && Event[PROTO].composedPath
   const data = IS_FIREFOX && Event[PROTO].composedPath
@@ -54,9 +52,8 @@ let pendingSetPopup;
   if (data.scripts) {
   if (data.scripts) {
     bridge.onScripts.forEach(fn => fn());
     bridge.onScripts.forEach(fn => fn());
     allow('SetTimeout', contentId);
     allow('SetTimeout', contentId);
-    allow('Pong', contentId);
     if (IS_FIREFOX) allow('InjectList', contentId);
     if (IS_FIREFOX) allow('InjectList', contentId);
-    await injectScripts(contentId, webId, data, isXml);
+    await injectScripts(contentId, webId, data);
   }
   }
   allow('VaultId', contentId);
   allow('VaultId', contentId);
   bridge.onScripts = null;
   bridge.onScripts = null;

+ 29 - 20
src/injected/content/inject.js

@@ -3,7 +3,10 @@ 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, getUniqIdSafe, isSameOriginWindow, log } from '../util';
+import {
+  NS_HTML, bindEvents, fireBridgeEvent,
+  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;
@@ -50,6 +53,19 @@ bridge.addHandlers({
 });
 });
 
 
 export function injectPageSandbox(contentId, webId) {
 export function injectPageSandbox(contentId, webId) {
+  const { cloneInto } = global;
+  /* A page can read our script's textContent in a same-origin iframe via DOMNodeRemoved event.
+   * Directly preventing it would require redefining ~20 DOM methods in the parent.
+   * Instead, we'll send the ids via a temporary handshakeId event, to which the web-bridge
+   * will listen only during its initial phase using vault-protected DOM methods. */
+  const handshakeId = getUniqIdSafe();
+  const handshaker = () => {
+    pageInjectable = true;
+    bindEvents(contentId, webId, bridge, cloneInto);
+    fireBridgeEvent(handshakeId + process.env.HANDSHAKE_ACK, [webId, contentId], cloneInto);
+  };
+  /* The vault contains safe methods that we got from the highest same-origin parent,
+   * where our code ran at document_start so it definitely predated the page scripts. */
   let vaultId = window[VAULT_SEED_NAME];
   let vaultId = window[VAULT_SEED_NAME];
   if (vaultId) {
   if (vaultId) {
     delete window[VAULT_SEED_NAME];
     delete window[VAULT_SEED_NAME];
@@ -60,33 +76,36 @@ export function injectPageSandbox(contentId, webId) {
       && tellParentToWriteVault(window.parent, getUniqIdSafe())
       && tellParentToWriteVault(window.parent, getUniqIdSafe())
       || '';
       || '';
   }
   }
+  /* With `once` the listener is removed before DOMNodeInserted is dispatched by appendChild,
+   * otherwise a same-origin parent page could use it to spoof the handshake. */
+  window::on(handshakeId, handshaker, { capture: true, once: true });
   inject({
   inject({
-    code: `(${VMInitInjection}('${vaultId}',${IS_FIREFOX}))('${webId}','${contentId}')`
+    code: `(${VMInitInjection}(${IS_FIREFOX},'${handshakeId}','${vaultId}'))()`
       + `\n//# sourceURL=${browser.runtime.getURL('sandbox/injected-web.js')}`,
       + `\n//# sourceURL=${browser.runtime.getURL('sandbox/injected-web.js')}`,
   });
   });
+  // Clean up in case CSP prevented the script from running
+  window::off(handshakeId, handshaker, true);
 }
 }
 
 
 /**
 /**
  * @param {string} contentId
  * @param {string} contentId
  * @param {string} webId
  * @param {string} webId
  * @param {VMGetInjectedData} data
  * @param {VMGetInjectedData} data
- * @param {boolean} isXml
  */
  */
-export async function injectScripts(contentId, webId, data, isXml) {
+export async function injectScripts(contentId, webId, data) {
   const { hasMore, info } = data;
   const { hasMore, info } = data;
-  pageInjectable = isXml ? false : null;
   realms = {
   realms = {
     __proto__: null,
     __proto__: null,
     /** @namespace VMInjectionRealm */
     /** @namespace VMInjectionRealm */
     [INJECT_CONTENT]: {
     [INJECT_CONTENT]: {
-      injectable: () => true,
+      injectable: true,
       /** @namespace VMRunAtLists */
       /** @namespace VMRunAtLists */
       lists: contLists = { start: [], body: [], end: [], idle: [] },
       lists: contLists = { start: [], body: [], end: [], idle: [] },
       is: 0,
       is: 0,
       info,
       info,
     },
     },
     [INJECT_PAGE]: {
     [INJECT_PAGE]: {
-      injectable: () => pageInjectable ?? checkInjectable(),
+      injectable: pageInjectable,
       lists: pgLists = { start: [], body: [], end: [], idle: [] },
       lists: pgLists = { start: [], body: [], end: [], idle: [] },
       is: 0,
       is: 0,
       info,
       info,
@@ -95,7 +114,7 @@ export async function injectScripts(contentId, webId, data, isXml) {
   const feedback = data.scripts.map((script) => {
   const feedback = data.scripts.map((script) => {
     const { id } = script.props;
     const { id } = script.props;
     // eslint-disable-next-line no-restricted-syntax
     // eslint-disable-next-line no-restricted-syntax
-    const realm = INJECT_MAPPING[script.injectInto].find(key => realms[key]?.injectable());
+    const realm = INJECT_MAPPING[script.injectInto].find(key => realms[key]?.injectable);
     // If the script wants this specific realm, which is unavailable, we won't inject it at all
     // If the script wants this specific realm, which is unavailable, we won't inject it at all
     if (realm) {
     if (realm) {
       const realmData = realms[realm];
       const realmData = realms[realm];
@@ -109,8 +128,8 @@ export async function injectScripts(contentId, webId, data, isXml) {
   });
   });
   const moreData = sendCmd('InjectionFeedback', {
   const moreData = sendCmd('InjectionFeedback', {
     feedback,
     feedback,
+    pageInjectable,
     feedId: data.feedId,
     feedId: data.feedId,
-    pageInjectable: pageInjectable ?? (hasMore && checkInjectable()),
   });
   });
   // saving while safe
   // saving while safe
   const getReadyState = hasMore && describeProperty(Document[PROTO], 'readyState').get;
   const getReadyState = hasMore && describeProperty(Document[PROTO], 'readyState').get;
@@ -168,16 +187,6 @@ async function injectDelayedScripts(contentId, webId, { cache, scripts }, getRea
   injectAll('idle');
   injectAll('idle');
 }
 }
 
 
-function checkInjectable() {
-  bridge.addHandlers({
-    Pong() {
-      pageInjectable = true;
-    },
-  }, true);
-  bridge.post('Ping');
-  return pageInjectable;
-}
-
 function inject(item) {
 function inject(item) {
   const script = document::createElementNS(NS_HTML, 'script');
   const script = document::createElementNS(NS_HTML, 'script');
   // Firefox ignores sourceURL comment when a syntax error occurs so we'll print the name manually
   // Firefox ignores sourceURL comment when a syntax error occurs so we'll print the name manually
@@ -232,7 +241,7 @@ async function injectList(runAt) {
 }
 }
 
 
 function setupContentInvoker(contentId, webId) {
 function setupContentInvoker(contentId, webId) {
-  const invokeContent = VMInitInjection('', IS_FIREFOX)(webId, contentId, bridge.onHandle);
+  const invokeContent = VMInitInjection(IS_FIREFOX)(webId, contentId, bridge.onHandle);
   const postViaBridge = bridge.post;
   const postViaBridge = bridge.post;
   bridge.post = (cmd, params, realm, node) => {
   bridge.post = (cmd, params, realm, node) => {
     const fn = realm === INJECT_CONTENT
     const fn = realm === INJECT_CONTENT

+ 7 - 4
src/injected/util/index.js

@@ -31,6 +31,12 @@ export const isSameOriginWindow = wnd => (
 // Avoiding the need to safe-guard a bunch of methods so we use just one
 // Avoiding the need to safe-guard a bunch of methods so we use just one
 export const getUniqIdSafe = (prefix = 'VM') => `${prefix}${mathRandom()}`;
 export const getUniqIdSafe = (prefix = 'VM') => `${prefix}${mathRandom()}`;
 
 
+export const fireBridgeEvent = (eventId, msg, cloneInto) => {
+  const detail = cloneInto ? cloneInto(msg, document) : msg;
+  const evtMain = new CustomEventSafe(eventId, { detail });
+  window::fire(evtMain);
+};
+
 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,
    * whereas MouseEvent (and some others) can't transfer objects without stringification. */
    * whereas MouseEvent (and some others) can't transfer objects without stringification. */
@@ -51,10 +57,7 @@ export const bindEvents = (srcId, destId, bridge, cloneInto) => {
   bridge.post = (cmd, data, { dataKey } = bridge, node) => {
   bridge.post = (cmd, data, { dataKey } = bridge, node) => {
     // Constructing the event now so we don't send anything if it throws on invalid `node`
     // Constructing the event now so we don't send anything if it throws on invalid `node`
     const evtNode = node && new MouseEventSafe(destId, { relatedTarget: node });
     const evtNode = node && new MouseEventSafe(destId, { relatedTarget: node });
-    const msg = { cmd, data, dataKey, node: !!evtNode };
-    const detail = cloneInto ? cloneInto(msg, document) : msg;
-    const evtMain = new CustomEventSafe(destId, { detail });
-    window::fire(evtMain);
+    fireBridgeEvent(destId, { cmd, data, dataKey, node: !!evtNode }, cloneInto);
     if (evtNode) window::fire(evtNode);
     if (evtNode) window::fire(evtNode);
   };
   };
 };
 };

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

@@ -23,6 +23,14 @@ export default function initialize(
   invokeHost,
   invokeHost,
 ) {
 ) {
   let invokeGuest;
   let invokeGuest;
+  if (process.env.HANDSHAKE_ID) {
+    window::on(process.env.HANDSHAKE_ID + process.env.HANDSHAKE_ACK, e => {
+      e = e::getDetail();
+      webId = e[0];
+      contentId = e[1];
+    }, { once: true });
+    window::fire(new CustomEventSafe(process.env.HANDSHAKE_ID));
+  }
   bridge.dataKey = contentId;
   bridge.dataKey = contentId;
   if (invokeHost) {
   if (invokeHost) {
     bridge.mode = INJECT_CONTENT;
     bridge.mode = INJECT_CONTENT;
@@ -43,9 +51,6 @@ export default function initialize(
       WriteVault(id) {
       WriteVault(id) {
         this[id] = VAULT;
         this[id] = VAULT;
       },
       },
-      Ping() {
-        bridge.post('Pong');
-      },
     });
     });
     setOwnProp(window, 'open', vmOwnFunc(function open(...args) {
     setOwnProp(window, 'open', vmOwnFunc(function open(...args) {
       const wnd = openWindow::apply(this, args);
       const wnd = openWindow::apply(this, args);