Преглед изворни кода

fix: survive on sites that abuse some known Symbols

* Symbol.isConcatSpreadable
* Symbol.replace
* also use setOwnProp/getOwnProp more
tophf пре 4 година
родитељ
комит
f5239b4e16

+ 2 - 2
scripts/webpack-protect-bootstrap-plugin.js

@@ -2,7 +2,7 @@ const escapeStringRegexp = require('escape-string-regexp');
 
 /**
  * WARNING! The following globals must be correctly assigned using wrapper-webpack-plugin.
- * toStringTag = Symbol.toStringTag
+ * toStringTagSym = Symbol.toStringTag
  * defineProperty = Object.defineProperty
  * hasOwnProperty = Object.prototype.hasOwnProperty
  * safeCall = Function.prototype.call.bind(Function.prototype.call)
@@ -30,7 +30,7 @@ class WebpackProtectBootstrapPlugin {
       ]]));
       hooks.requireExtensions.tap(NAME, src => replace(src, [
         ["(typeof Symbol !== 'undefined' && Symbol.toStringTag)", '(true)'],
-        ['Symbol.toStringTag', 'toStringTag'],
+        ['Symbol.toStringTag', 'toStringTagSym'],
         [/Object\.(defineProperty\([^){\n]+{)/g, `$1${NULL_PROTO},`],
         ['Object.create(null)', NULL_OBJ],
         ['for(var key in value)', 'for(const key in value)'],

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

@@ -37,7 +37,7 @@ export const {
 } = Object;
 export const { random: mathRandom } = Math;
 export const regexpTest = RegExp[PROTO].test;
-export const { toStringTag } = Symbol; // used by ProtectWebpackBootstrapPlugin
+export const { toStringTag: toStringTagSym } = Symbol; // used by ProtectWebpackBootstrapPlugin
 export const { decode: tdDecode } = TextDecoderSafe[PROTO];
 export const { stopImmediatePropagation } = Event[PROTO];
 export const { get: getHref } = describeProperty(HTMLAnchorElement[PROTO], 'href');

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

@@ -20,9 +20,12 @@ export const WINDOW_FOCUS = 'window.focus';
 export const NS_HTML = 'http://www.w3.org/1999/xhtml';
 export const CALLBACK_ID = '__CBID';
 
+export const getObjectTypeTag = val => val && val::objectToString()::slice(8, -1);
+
 export const isFunction = val => typeof val === 'function';
 export const isObject = val => val !== null && typeof val === 'object';
-export const isPromise = val => val && val::objectToString() === '[object Promise]';
+// TODO: maybe use `val[toStringTagSym]` when strict_min_version > 78
+export const isPromise = val => getObjectTypeTag(val) === 'Promise';
 export const isString = val => typeof val === 'string';
 
 export const getOwnProp = (obj, key) => (

+ 4 - 6
src/injected/web/gm-api-wrapper.js

@@ -1,7 +1,7 @@
 import bridge from './bridge';
 import { makeGmApi } from './gm-api';
 import { makeGlobalWrapper } from './gm-global-wrapper';
-import { makeComponentUtils } from './util-web';
+import { makeComponentUtils, safeConcat } from './util-web';
 
 /** Name in Greasemonkey4 -> name in GM */
 const GM4_ALIAS = {
@@ -28,7 +28,7 @@ export function makeGmApiWrapper(script) {
   // Add GM functions
   // Reference: http://wiki.greasespot.net/Greasemonkey_Manual:API
   const { meta } = script;
-  const grant = meta.grant || [];
+  const grant = meta.grant;
   if (grant.length === 1 && grant[0] === 'none') {
     grant.length = 0;
   }
@@ -64,9 +64,7 @@ export function makeGmApiWrapper(script) {
   }
   if (!gmApi && grant.length) gmApi = makeGmApi();
   grant::forEach((name) => {
-    // Spoofed String index getters won't be called within length, length itself is unforgeable
-    const gm4name = name.length > 3 && name[2] === '.' && name[0] === 'G' && name[1] === 'M'
-      && name::slice(3);
+    const gm4name = name::slice(0, 3) === 'GM.' && name::slice(3);
     const fn = gmApi[gm4name ? `GM_${GM4_ALIAS[gm4name] || gm4name}` : name];
     if (fn) {
       if (gm4name) {
@@ -94,7 +92,7 @@ function makeGmInfo(script, resources) {
     case 'exclude': // -> excludes
     case 'include': // -> includes
       key += 's';
-      val = []::concat(val);
+      val = safeConcat([], val);
       break;
     default:
     }

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

@@ -195,15 +195,12 @@ function webAddElement(parent, tag, attrs, context) {
      but we keep it for compatibility with GM_addStyle in VM of 2017-2019
      https://github.com/violentmonkey/violentmonkey/issues/217
      as well as for GM_addElement in Tampermonkey. */
-  safeDefineProperty(el, 'then', {
-    configurable: true,
-    value(callback) {
-      // prevent infinite resolve loop
-      delete el.then;
-      callback(el);
-    },
-  });
-  return el;
+  return setOwnProp(el, 'then', async cb => (
+    // Preventing infinite resolve loop
+    delete el.then
+    // Native Promise ignores non-function
+    && (isFunction(cb) ? cb(el) : el)
+  ));
 }
 
 function getResource(context, name, isBlob) {

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

@@ -1,11 +1,11 @@
 import { INJECT_CONTENT } from '../util';
 import bridge from './bridge';
-import { FastLookup } from './util-web';
+import { FastLookup, safeConcat } from './util-web';
 
 /** 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 */
 const isFrameIndex = key => key >= 0 && key <= 0xFFFF_FFFE && key === `${+key}`;
-
+const scopeSym = SymbolSafe.unscopables;
 const globalKeysSet = FastLookup();
 const globalKeys = (function makeGlobalKeys() {
   const kWrappedJSObject = 'wrappedJSObject';
@@ -164,7 +164,7 @@ export function makeGlobalWrapper(local) {
   /* 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, toStringTag, { get: () => 'Window' });
+  safeDefineProperty(local, toStringTagSym, { get: () => 'Window' });
   const wrapper = new ProxySafe(local, {
     __proto__: null,
     defineProperty(_, name, desc) {
@@ -245,12 +245,13 @@ function makeOwnKeys(local, globals) {
   const names = getOwnPropertyNames(local)::filter(notIncludedIn, globals);
   const symbols = getOwnPropertySymbols(local)::filter(notIncludedIn, globals);
   const frameIndexes = [];
-  for (let i = 0, s; (global[s = `${i}`] || 0)::objectToString() === '[object Window]'; i += 1) {
+  for (let i = 0, s; getObjectTypeTag(global[s = `${i}`]) === 'Window'; i += 1) {
     if (!(s in local)) {
       setOwnProp(frameIndexes, s, s);
     }
   }
-  return []::concat(
+  return safeConcat(
+    [],
     globals === globalKeysSet ? globalKeys : globals.toArray(),
     frameIndexes,
     names,

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

@@ -36,7 +36,7 @@ function parseData(req, msg) {
   case 'document':
     res = new DOMParserSafe()::parseFromString(res,
       // Cutting everything after , or ; and trimming whitespace
-      msg.contentType::replace(/[,;].*|\s+/g, '') || 'text/html');
+      /[,;].*|\s+/g::regexpReplace(msg.contentType, '') || 'text/html');
     break;
   default:
   }
@@ -70,11 +70,10 @@ function callback(req, msg) {
       },
     });
     if (headers != null) req.headers = headers;
-    // Spoofed String/Array index getters won't be called within length, length itself is unforgeable
-    if (text != null) req.text = text.length && text[0] === 'same' ? response : text;
-    data.context = opts.context;
-    data.responseHeaders = req.headers;
-    data.responseText = req.text;
+    if (text != null) req.text = getOwnProp(text, 0) === 'same' ? response : text;
+    setOwnProp(data, 'context', opts.context);
+    setOwnProp(data, 'responseHeaders', req.headers);
+    setOwnProp(data, 'responseText', req.text);
     cb(data);
   }
   if (msg.type === 'loadend') delete idMap[req.id];
@@ -144,7 +143,7 @@ function getResponseType({ responseType = '' }) {
  * and ReadableStream, which Chrome can't transfer to isolated world via CustomEvent.
  */
 async function encodeBody(body) {
-  const wasBlob = body::objectToString() === '[object Blob]';
+  const wasBlob = getObjectTypeTag(body) === 'Blob';
   const blob = wasBlob ? body : await new ResponseSafe(body)::safeResponseBlob();
   const reader = new FileReaderSafe();
   return new PromiseSafe(resolve => {

+ 28 - 28
src/injected/web/safe-globals-web.js

@@ -1,5 +1,5 @@
 /* eslint-disable one-var, one-var-declaration-per-line, no-unused-vars,
-   prefer-const, import/no-mutable-exports, no-restricted-syntax */
+   prefer-const, import/no-mutable-exports */
 
 /**
  * `safeCall` is used by our modified babel-plugin-safe-bind.js.
@@ -19,14 +19,14 @@ export let
   PromiseSafe,
   ProxySafe,
   ResponseSafe,
+  SymbolSafe,
   fire,
   off,
   on,
   openWindow,
   safeIsFinite,
   // Symbol
-  scopeSym,
-  toStringTag,
+  toStringTagSym,
   // Object
   apply,
   assign,
@@ -52,7 +52,6 @@ export let
   // String.prototype
   charCodeAt,
   slice,
-  replace,
   // safeCall
   safeCall,
   // various methods
@@ -66,6 +65,7 @@ export let
   readAsDataURL, // FileReader
   safeResponseBlob, // Response - safe = "safe global" to disambiguate the name
   stopImmediatePropagation,
+  regexpReplace,
   then,
   // various getters
   getBlobType, // Blob
@@ -82,11 +82,12 @@ export let
 export const VAULT = (() => {
   let ArrayP;
   let ElementP;
+  let ObjectSafe;
   let StringP;
   let i = -1;
+  let call;
   let res;
-  let src = window;
-  let srcFF;
+  let src = global; // FF defines some stuff only on `global` in content mode
   if (process.env.VAULT_ID) {
     res = window[process.env.VAULT_ID];
     delete window[process.env.VAULT_ID];
@@ -97,7 +98,6 @@ export const VAULT = (() => {
     src = res[0];
     res = createNullObj();
   }
-  srcFF = global === window ? src : global;
   res = [
     // window
     BlobSafe = res[i += 1] || src.Blob,
@@ -109,30 +109,28 @@ export const VAULT = (() => {
     MouseEventSafe = res[i += 1] || src.MouseEvent,
     Object = res[i += 1] || src.Object,
     PromiseSafe = res[i += 1] || src.Promise,
+    SymbolSafe = res[i += 1] || src.Symbol,
     // In FF content mode global.Proxy !== window.Proxy
-    ProxySafe = res[i += 1] || srcFF.Proxy,
+    ProxySafe = res[i += 1] || src.Proxy,
     ResponseSafe = res[i += 1] || src.Response,
     fire = res[i += 1] || src.dispatchEvent,
-    safeIsFinite = res[i += 1] || srcFF.isFinite, // Firefox defines `isFinite` on `global`
+    safeIsFinite = res[i += 1] || src.isFinite, // Firefox defines `isFinite` on `global`
     off = res[i += 1] || src.removeEventListener,
     on = res[i += 1] || src.addEventListener,
     openWindow = res[i += 1] || src.open,
-    // Symbol
-    scopeSym = res[i += 1] || srcFF.Symbol.unscopables,
-    toStringTag = res[i += 1] || srcFF.Symbol.toStringTag,
-    // Object
-    describeProperty = res[i += 1] || Object.getOwnPropertyDescriptor,
-    defineProperty = res[i += 1] || Object.defineProperty,
-    getOwnPropertyNames = res[i += 1] || Object.getOwnPropertyNames,
-    getOwnPropertySymbols = res[i += 1] || Object.getOwnPropertySymbols,
-    assign = res[i += 1] || Object.assign,
-    objectKeys = res[i += 1] || Object.keys,
-    objectValues = res[i += 1] || Object.values,
-    apply = res[i += 1] || Object.apply,
-    bind = res[i += 1] || Object.bind,
+    // Object - using ObjectSafe to pacify eslint without disabling the rule
+    defineProperty = (ObjectSafe = Object) && res[i += 1] || ObjectSafe.defineProperty,
+    describeProperty = res[i += 1] || ObjectSafe.getOwnPropertyDescriptor,
+    getOwnPropertyNames = res[i += 1] || ObjectSafe.getOwnPropertyNames,
+    getOwnPropertySymbols = res[i += 1] || ObjectSafe.getOwnPropertySymbols,
+    assign = res[i += 1] || ObjectSafe.assign,
+    objectKeys = res[i += 1] || ObjectSafe.keys,
+    objectValues = res[i += 1] || ObjectSafe.values,
+    apply = res[i += 1] || ObjectSafe.apply,
+    bind = res[i += 1] || ObjectSafe.bind,
     // Object.prototype
-    hasOwnProperty = res[i += 1] || Object[PROTO].hasOwnProperty,
-    objectToString = res[i += 1] || Object[PROTO].toString,
+    hasOwnProperty = res[i += 1] || ObjectSafe[PROTO].hasOwnProperty,
+    objectToString = res[i += 1] || ObjectSafe[PROTO].toString,
     // Array.prototype
     concat = res[i += 1] || (ArrayP = src.Array[PROTO]).concat,
     filter = res[i += 1] || ArrayP.filter,
@@ -141,22 +139,22 @@ export const VAULT = (() => {
     // Element.prototype
     remove = res[i += 1] || (ElementP = src.Element[PROTO]).remove,
     // String.prototype
-    charCodeAt = res[i += 1] || (StringP = srcFF.String[PROTO]).charCodeAt,
+    charCodeAt = res[i += 1] || (StringP = src.String[PROTO]).charCodeAt,
     slice = res[i += 1] || StringP.slice,
-    replace = res[i += 1] || StringP.replace,
     // safeCall
-    safeCall = res[i += 1] || Object.call.bind(Object.call),
+    safeCall = res[i += 1] || (call = ObjectSafe.call).bind(call),
     // various methods
     createObjectURL = res[i += 1] || src.URL.createObjectURL,
     funcToString = res[i += 1] || safeCall.toString,
     ArrayIsArray = res[i += 1] || src.Array.isArray,
     jsonParse = res[i += 1] || src.JSON.parse,
     logging = res[i += 1] || assign({ __proto__: null }, src.console),
-    mathRandom = res[i += 1] || srcFF.Math.random,
+    mathRandom = res[i += 1] || src.Math.random,
     parseFromString = res[i += 1] || DOMParserSafe[PROTO].parseFromString,
     readAsDataURL = res[i += 1] || FileReaderSafe[PROTO].readAsDataURL,
     safeResponseBlob = res[i += 1] || ResponseSafe[PROTO].blob,
     stopImmediatePropagation = res[i += 1] || src.Event[PROTO].stopImmediatePropagation,
+    regexpReplace = res[i += 1] || src.RegExp[PROTO][SymbolSafe.replace],
     then = res[i += 1] || PromiseSafe[PROTO].then,
     // various getters
     getBlobType = res[i += 1] || describeProperty(BlobSafe[PROTO], 'type').get,
@@ -165,5 +163,7 @@ export const VAULT = (() => {
     getReaderResult = res[i += 1] || describeProperty(FileReaderSafe[PROTO], 'result').get,
     getRelatedTarget = res[i += 1] || describeProperty(MouseEventSafe[PROTO], 'relatedTarget').get,
   ];
+  // Well-known Symbols are unforgeable
+  toStringTagSym = SymbolSafe.toStringTag;
   return res;
 })();

+ 12 - 2
src/injected/web/util-web.js

@@ -1,6 +1,16 @@
 import { INJECT_CONTENT } from '../util';
 import bridge from './bridge';
 
+const isConcatSpreadableSym = SymbolSafe.isConcatSpreadable;
+
+export const safeConcat = (dest, ...arrays) => {
+  if (!dest[isConcatSpreadableSym]) {
+    setOwnProp(dest, isConcatSpreadableSym, true);
+    arrays::forEach(arr => setOwnProp(arr, isConcatSpreadableSym, true));
+  }
+  return concat::apply(dest, arrays);
+};
+
 // Reference: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/JSON#Polyfill
 const escMap = {
   __proto__: null,
@@ -38,7 +48,7 @@ export const jsonDump = (value, stack) => {
     res = `${value}`;
     break;
   case 'string':
-    res = `"${value::replace(escRE, escFunc)}"`;
+    res = `"${escRE::regexpReplace(value, escFunc)}"`;
     break;
   case 'object':
     if (!stack) {
@@ -97,7 +107,7 @@ export const FastLookup = (hubs = createNullObj()) => {
     toArray: () => {
       const values = objectValues(hubs);
       values::forEach((val, i) => { values[i] = objectKeys(val); });
-      return concat::apply([], values);
+      return safeConcat([], values);
     },
   };
   function getHub(val, autoCreate) {