瀏覽代碼

fix: get rid of RegExp in `web`, #1421, #1431

tophf 3 年之前
父節點
當前提交
7c565c03a2
共有 5 個文件被更改,包括 70 次插入82 次删除
  1. 44 30
      .eslintrc.js
  2. 1 1
      src/injected/web/gm-values.js
  3. 18 5
      src/injected/web/requests.js
  4. 2 9
      src/injected/web/safe-globals-web.js
  5. 5 37
      src/injected/web/util-web.js

+ 44 - 30
.eslintrc.js

@@ -25,6 +25,38 @@ const GLOBALS_WEB = {
   IS_FIREFOX: false, // passed as a parameter to VMInitInjection in webpack.conf.js
 };
 
+const INJECTED_RULES = {
+  'no-restricted-imports': ['error', {
+    patterns: ['*/common', '*/common/*'],
+  }],
+  'no-restricted-syntax': [
+    'error', {
+      selector: 'ObjectExpression > ExperimentalSpreadProperty',
+      message: 'Object spread adds a polyfill in injected* even if unused by it',
+    }, {
+      selector: 'OptionalCallExpression',
+      message: 'Optional call uses .call(), which may be spoofed/broken in an unsafe environment',
+      // TODO: write a Babel plugin to use safeCall for this.
+    }, {
+      selector: 'ArrayPattern',
+      message: 'Destructuring via Symbol.iterator may be spoofed/broken in an unsafe environment',
+    }, {
+      selector: ':matches(ArrayExpression, CallExpression) > SpreadElement',
+      message: 'Spreading via Symbol.iterator may be spoofed/broken in an unsafe environment',
+    }, {
+      selector: '[callee.object.name="Object"], MemberExpression[object.name="Object"]',
+      message: 'Using potentially spoofed methods in an unsafe environment',
+      // TODO: auto-generate the rule using GLOBALS
+    }, {
+      selector: `CallExpression[callee.name="defineProperty"]:not(${[
+        '[arguments.2.properties.0.key.name="__proto__"]',
+        ':has(CallExpression[callee.name="createNullObj"])'
+      ].join(',')})`,
+      message: 'Prototype of descriptor may be spoofed/broken in an unsafe environment',
+    }
+  ],
+};
+
 module.exports = {
   root: true,
   extends: [
@@ -63,37 +95,19 @@ module.exports = {
     ), {}),
   }, {
     files: [...FILES_INJECTED, ...FILES_SHARED],
+    rules: INJECTED_RULES,
+  }, {
+    files: FILES_WEB,
     rules: {
-      'no-restricted-imports': ['error', {
-        patterns: ['*/common', '*/common/*'],
-      }],
-      /* Our .browserslistrc targets old browsers so the compiled code for {...objSpread} uses
-         babel's polyfill that calls methods like `Object.assign` instead of our safe `assign`.
-         Ideally, `eslint-plugin-compat` should be used but I couldn't make it work. */
-      'no-restricted-syntax': ['error', {
-        selector: 'ObjectExpression > ExperimentalSpreadProperty',
-        message: 'Object spread adds a polyfill in injected* even if unused by it',
-      }, {
-        selector: 'OptionalCallExpression',
-        message: 'Optional call uses .call(), which may be spoofed/broken in an unsafe environment',
-        // TODO: write a Babel plugin to use safeCall for this.
-      }, {
-        selector: 'ArrayPattern',
-        message: 'Destructuring via Symbol.iterator may be spoofed/broken in an unsafe environment',
-      }, {
-        selector: ':matches(ArrayExpression, CallExpression) > SpreadElement',
-        message: 'Spreading via Symbol.iterator may be spoofed/broken in an unsafe environment',
-      }, {
-        selector: '[callee.object.name="Object"], MemberExpression[object.name="Object"]',
-        message: 'Using potentially spoofed methods in an unsafe environment',
-        // TODO: auto-generate the rule using GLOBALS
-      }, {
-        selector: `CallExpression[callee.name="defineProperty"]:not(${[
-          '[arguments.2.properties.0.key.name="__proto__"]',
-          ':has(CallExpression[callee.name="createNullObj"])'
-        ].join(',')})`,
-        message: 'Prototype of descriptor may be spoofed/broken in an unsafe environment',
-      }],
+      ...INJECTED_RULES,
+      'no-restricted-syntax': [
+        ...INJECTED_RULES['no-restricted-syntax'],
+        {
+          selector: '[regex], NewExpression[callee.name="RegExp"]',
+          message: 'RegExp internally depends on a *ton* of stuff that may be spoofed or broken',
+          // https://262.ecma-international.org/12.0/#sec-regexpexec
+        },
+      ],
     },
   }, {
     // build scripts

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

@@ -6,7 +6,7 @@ export const changeHooks = createNullObj();
 
 const dataDecoders = {
   __proto__: null,
-  o: jsonParse,
+  o: SafeJSON.parse,
   n: val => +val,
   b: val => val === 'true',
 };

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

@@ -1,7 +1,6 @@
 import bridge from './bridge';
 
 const idMap = createNullObj();
-const contentTypeRe = setOwnProp(/[,;].*|\s+/g, 'exec', regexpExec);
 
 bridge.addHandlers({
   HttpRequested(msg) {
@@ -32,12 +31,10 @@ function parseData(req, msg) {
   let res = req.raw;
   switch (req.opts.responseType) {
   case 'json':
-    res = jsonParse(res);
+    res = SafeJSON.parse(res);
     break;
   case 'document':
-    res = new SafeDOMParser()::parseFromString(res,
-      // Cutting everything after , or ; and trimming whitespace
-      contentTypeRe::regexpReplace(msg.contentType, '') || 'text/html');
+    res = new SafeDOMParser()::parseFromString(res, getContentType(msg) || 'text/html');
     break;
   default:
   }
@@ -48,6 +45,22 @@ function parseData(req, msg) {
   return res;
 }
 
+/**
+ * Not using RegExp because it internally depends on proto stuff that can be easily broken,
+ * and safe-guarding all of it is ridiculously disproportional.
+ */
+function getContentType(msg) {
+  const type = msg.contentType || '';
+  const len = type.length;
+  let i = 0;
+  let c;
+  // Cutting everything after , or ; or whitespace
+  while (i < len && (c = type[i]) !== ',' && c !== ';' && c > ' ') {
+    i += 1;
+  }
+  return type::slice(0, i);
+}
+
 // request object functions
 function callback(req, msg) {
   const { opts } = req;

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

@@ -13,6 +13,7 @@ export let
   SafeError,
   SafeEventTarget,
   SafeFileReader,
+  SafeJSON,
   SafeKeyboardEvent,
   SafeMouseEvent,
   Object,
@@ -23,7 +24,6 @@ export let
   fire,
   off,
   on,
-  safeIsFinite,
   // Symbol
   toStringTagSym,
   // Object
@@ -57,15 +57,12 @@ export let
   createObjectURL,
   funcToString,
   ArrayIsArray,
-  jsonParse,
   logging,
   mathRandom,
   parseFromString, // DOMParser
   readAsDataURL, // FileReader
   safeResponseBlob, // Response - safe = "safe global" to disambiguate the name
   stopImmediatePropagation,
-  regexpExec, // used by replace() internally
-  regexpReplace,
   then,
   // various getters
   getBlobType, // Blob
@@ -82,7 +79,6 @@ export let
 export const VAULT = (() => {
   let ArrayP;
   let ElementP;
-  let RegExpP;
   let SafeObject;
   let StringP;
   let i = -1;
@@ -106,6 +102,7 @@ export const VAULT = (() => {
     SafeError = res[i += 1] || src.Error,
     SafeEventTarget = res[i += 1] || src.EventTarget,
     SafeFileReader = res[i += 1] || src.FileReader,
+    SafeJSON = res[i += 1] || src.JSON,
     SafeKeyboardEvent = res[i += 1] || src.KeyboardEvent,
     SafeMouseEvent = res[i += 1] || src.MouseEvent,
     Object = res[i += 1] || src.Object,
@@ -115,7 +112,6 @@ export const VAULT = (() => {
     SafeProxy = res[i += 1] || src.Proxy,
     SafeResponse = res[i += 1] || src.Response,
     fire = res[i += 1] || src.dispatchEvent,
-    safeIsFinite = res[i += 1] || src.isFinite, // Firefox defines `isFinite` on `global`
     off = res[i += 1] || src.removeEventListener,
     on = res[i += 1] || src.addEventListener,
     // Object - using SafeObject to pacify eslint without disabling the rule
@@ -147,15 +143,12 @@ export const VAULT = (() => {
     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] || src.Math.random,
     parseFromString = res[i += 1] || SafeDOMParser[PROTO].parseFromString,
     readAsDataURL = res[i += 1] || SafeFileReader[PROTO].readAsDataURL,
     safeResponseBlob = res[i += 1] || SafeResponse[PROTO].blob,
     stopImmediatePropagation = res[i += 1] || src.Event[PROTO].stopImmediatePropagation,
-    regexpExec = res[i += 1] || (RegExpP = src.RegExp[PROTO]).exec,
-    regexpReplace = res[i += 1] || RegExpP[SafeSymbol.replace],
     then = res[i += 1] || SafePromise[PROTO].then,
     // various getters
     getBlobType = res[i += 1] || describeProperty(src.Blob[PROTO], 'type').get,

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

@@ -11,47 +11,15 @@ export const safeConcat = (dest, ...arrays) => {
   return concat::apply(dest, arrays);
 };
 
-// Reference: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/JSON#Polyfill
-const escMap = {
-  __proto__: null,
-  '"': '\\"',
-  '\\': '\\\\',
-  '\b': '\\b',
-  '\f': '\\f',
-  '\n': '\\n',
-  '\r': '\\r',
-  '\t': '\\t',
-};
-// TODO: handle \u2028\u2029 when Chrome's JSON.stringify starts to escape them
-// eslint-disable-next-line no-control-regex
-const escRE = setOwnProp(/[\\"\u0000-\u001F]/g, 'exec', regexpExec);
-const hex = '0123456789ABCDEF';
-const escCharCode = num => `\\u00${
-  hex[num >> 4] // eslint-disable-line no-bitwise
-}${
-  hex[num % 16]
-}`;
-const escFunc = m => escMap[m] || escCharCode(m::charCodeAt(0));
 /**
  * When running in the page context we must beware of sites that override Array#toJSON
  * leading to an invalid result, which is why our jsonDump() ignores toJSON.
- * Thus, we use the native JSON.stringify() only in the content script context and only until
- * a userscript is injected into this context (due to `@inject-into` and/or a CSP problem).
  */
 export const jsonDump = (value, stack) => {
   let res;
-  switch (value === null ? (res = 'null') : typeof value) {
-  case 'bigint':
-  case 'number':
-    res = safeIsFinite(value) ? `${value}` : 'null';
-    break;
-  case 'boolean':
-    res = `${value}`;
-    break;
-  case 'string':
-    res = `"${escRE::regexpReplace(value, escFunc)}"`;
-    break;
-  case 'object':
+  if (value === null) {
+    res = 'null';
+  } else if (typeof value === 'object') {
     if (!stack) {
       stack = [value]; // Creating the array here, only when type is object.
     } else if (stack::indexOf(value) >= 0) {
@@ -78,8 +46,8 @@ export const jsonDump = (value, stack) => {
       res += '}';
     }
     stack.length -= 1;
-    break;
-  default:
+  } else if (value !== undefined) {
+    res = SafeJSON.stringify(value);
   }
   return res;
 };