Selaa lähdekoodia

fix: apply network blacklist to more scenarios

...to block access via @resource+GM_getResourceText and so on
tophf 1 vuosi sitten
vanhempi
sitoutus
8081d77842

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

@@ -1,6 +1,6 @@
 import {
 import {
   compareVersion, dataUri2text, i18n, getScriptHome, isDataUri,
   compareVersion, dataUri2text, i18n, getScriptHome, isDataUri,
-  getFullUrl, getScriptName, getScriptUpdateUrl, isRemote, sendCmd, trueJoin,
+  getScriptName, getScriptUpdateUrl, isRemote, sendCmd, trueJoin,
   getScriptPrettyUrl, getScriptRunAt, makePause, isValidHttpUrl, normalizeTag,
   getScriptPrettyUrl, getScriptRunAt, makePause, isValidHttpUrl, normalizeTag,
   ignoreChromeErrors,
   ignoreChromeErrors,
 } from '@/common';
 } from '@/common';
@@ -19,6 +19,7 @@ import storage, {
 } from './storage';
 } from './storage';
 import { storageCacheHas } from './storage-cache';
 import { storageCacheHas } from './storage-cache';
 import { reloadTabForScript } from './tabs';
 import { reloadTabForScript } from './tabs';
+import { vetUrl } from './url';
 
 
 let maxScriptId = 0;
 let maxScriptId = 0;
 let maxScriptPosition = 0;
 let maxScriptPosition = 0;
@@ -698,7 +699,7 @@ function buildPathMap(script, base) {
     meta.icon,
     meta.icon,
   ].reduce((map, key) => {
   ].reduce((map, key) => {
     if (key) {
     if (key) {
-      const fullUrl = getFullUrl(key, baseUrl);
+      const fullUrl = vetUrl(key, baseUrl);
       if (fullUrl !== key) map[key] = fullUrl;
       if (fullUrl !== key) map[key] = fullUrl;
     }
     }
     return map;
     return map;

+ 3 - 2
src/background/utils/notifications.js

@@ -1,6 +1,7 @@
-import { i18n, defaultImage, sendTabCmd, trueJoin, getFullUrl } from '@/common';
+import { i18n, defaultImage, sendTabCmd, trueJoin } from '@/common';
 import { addPublicCommands, commands } from './init';
 import { addPublicCommands, commands } from './init';
 import { CHROME } from './ua';
 import { CHROME } from './ua';
+import { vetUrl } from './url';
 
 
 /** @type {{ [nid: string]: browser.runtime.MessageSender | function | number }} */
 /** @type {{ [nid: string]: browser.runtime.MessageSender | function | number }} */
 const openers = {};
 const openers = {};
@@ -38,7 +39,7 @@ addPublicCommands({
     } else if (src) {
     } else if (src) {
       openers[notificationId] = src;
       openers[notificationId] = src;
       if (+zombieTimeout > 0) src[kZombieTimeout] = +zombieTimeout;
       if (+zombieTimeout > 0) src[kZombieTimeout] = +zombieTimeout;
-      if (zombieUrl != null) src[kZombieUrl] = getFullUrl(zombieUrl, src.url);
+      if (zombieUrl != null) src[kZombieUrl] = vetUrl(zombieUrl, src.url);
     }
     }
     return notificationId;
     return notificationId;
   },
   },

+ 3 - 4
src/background/utils/requests.js

@@ -1,4 +1,4 @@
-import { blob2base64, getFullUrl, sendTabCmd, string2uint8array } from '@/common';
+import { blob2base64, sendTabCmd, string2uint8array } from '@/common';
 import { CHARSET_UTF8, FORM_URLENCODED, UA_PROPS } from '@/common/consts';
 import { CHARSET_UTF8, FORM_URLENCODED, UA_PROPS } from '@/common/consts';
 import { downloadBlob } from '@/common/download';
 import { downloadBlob } from '@/common/download';
 import { deepEqual, forEachEntry, forEachValue, objectPick } from '@/common/object';
 import { deepEqual, forEachEntry, forEachValue, objectPick } from '@/common/object';
@@ -8,8 +8,8 @@ import {
   FORBIDDEN_HEADER_RE, VM_VERIFY, requests, toggleHeaderInjector, verify, kCookie, kSetCookie,
   FORBIDDEN_HEADER_RE, VM_VERIFY, requests, toggleHeaderInjector, verify, kCookie, kSetCookie,
 } from './requests-core';
 } from './requests-core';
 import { getFrameDocIdAsObj, getFrameDocIdFromSrc } from './tabs';
 import { getFrameDocIdAsObj, getFrameDocIdFromSrc } from './tabs';
-import { testBlacklistNet } from './tester';
 import { FIREFOX, navUA, navUAD } from './ua';
 import { FIREFOX, navUA, navUAD } from './ua';
+import { vetUrl } from './url';
 
 
 addPublicCommands({
 addPublicCommands({
   /**
   /**
@@ -211,10 +211,9 @@ async function httpRequest(opts, events, src, cb) {
   const { tab } = src;
   const { tab } = src;
   const { incognito } = tab;
   const { incognito } = tab;
   const { anonymous, id, overrideMimeType, [kXhrType]: xhrType } = opts;
   const { anonymous, id, overrideMimeType, [kXhrType]: xhrType } = opts;
-  const url = getFullUrl(opts.url, src.url);
+  const url = vetUrl(opts.url, src.url, true);
   const req = requests[id];
   const req = requests[id];
   if (!req || req.cb) return;
   if (!req || req.cb) return;
-  if (testBlacklistNet(url)) throw 'Not allowed to access a blacklisted URL ' + url;
   req.cb = cb;
   req.cb = cb;
   req[kFileName] = opts[kFileName];
   req[kFileName] = opts[kFileName];
   const { xhr } = req;
   const { xhr } = req;

+ 1 - 5
src/background/utils/storage-fetch.js

@@ -1,12 +1,8 @@
 import { isCdnUrlRe, isDataUri, isRemote, makeRaw, request } from '@/common';
 import { isCdnUrlRe, isDataUri, isRemote, makeRaw, request } from '@/common';
 import { NO_CACHE } from '@/common/consts';
 import { NO_CACHE } from '@/common/consts';
-import limitConcurrency from '@/common/limit-concurrency';
 import storage from './storage';
 import storage from './storage';
 import { getUpdateInterval } from './update';
 import { getUpdateInterval } from './update';
-
-const requestLimited = limitConcurrency(request, 4, 100, 1000,
-  url => url.split('/')[2] // simple extraction of the `host` part
-);
+import { requestLimited } from './url';
 
 
 storage.cache.fetch = cacheOrFetch({
 storage.cache.fetch = cacheOrFetch({
   init: options => ({ ...options, [kResponseType]: 'blob' }),
   init: options => ({ ...options, [kResponseType]: 'blob' }),

+ 3 - 2
src/background/utils/tabs.js

@@ -1,9 +1,10 @@
-import { browserWindows, getActiveTab, noop, sendTabCmd, getFullUrl } from '@/common';
+import { browserWindows, getActiveTab, noop, sendTabCmd } from '@/common';
 import { getDomain } from '@/common/tld';
 import { getDomain } from '@/common/tld';
 import { addOwnCommands, addPublicCommands, commands } from './init';
 import { addOwnCommands, addPublicCommands, commands } from './init';
 import { getOption } from './options';
 import { getOption } from './options';
 import { testScript } from './tester';
 import { testScript } from './tester';
 import { CHROME, FIREFOX } from './ua';
 import { CHROME, FIREFOX } from './ua';
+import { vetUrl } from './url';
 
 
 const openers = {};
 const openers = {};
 const openerTabIdSupported = !IS_FIREFOX // supported in Chrome
 const openerTabIdSupported = !IS_FIREFOX // supported in Chrome
@@ -119,7 +120,7 @@ addPublicCommands({
     if (!/^[-\w]+:/.test(url)) {
     if (!/^[-\w]+:/.test(url)) {
       url = isInternal
       url = isInternal
         ? browser.runtime.getURL(url)
         ? browser.runtime.getURL(url)
-        : getFullUrl(url, srcUrl);
+        : vetUrl(url, srcUrl);
     }
     }
     if (isInternal
     if (isInternal
         && url.startsWith(EDITOR_ROUTE)
         && url.startsWith(EDITOR_ROUTE)

+ 47 - 0
src/background/utils/url.js

@@ -0,0 +1,47 @@
+import { isCdnUrlRe, isRemote, makeRaw, request } from '@/common';
+import { VM_HOME } from '@/common/consts';
+import limitConcurrency from '@/common/limit-concurrency';
+import { addOwnCommands } from './init';
+import { testBlacklistNet } from './tester';
+
+export const requestLimited = limitConcurrency(request, 4, 100, 1000,
+  url => url.split('/')[2] // simple extraction of the `host` part
+);
+
+addOwnCommands({
+  async Request({ url, ...opts }) {
+    const vettedUrl = vetUrl(url);
+    const fn = isRemote(vettedUrl) && !isCdnUrlRe.test(vettedUrl)
+      ? requestLimited
+      : request;
+    const res = await fn(vettedUrl, opts);
+    if (opts[kResponseType] === 'blob') {
+      res.data = await makeRaw(res);
+    }
+    return res;
+  },
+});
+
+/**
+ * @param {string} url
+ * @param {string} [base]
+ * @param {boolean} [throwOnFailure]
+ * @returns {string} a resolved `url` or `data:,Invalid URL ${url}`
+ */
+export function vetUrl(url, base = VM_HOME, throwOnFailure) {
+  let res, err;
+  try {
+    res = new URL(url, base).href;
+    if (res.startsWith(extensionRoot) || testBlacklistNet(res)) {
+      err = 'Blacklisted';
+    }
+  } catch {
+    err = 'Invalid';
+  }
+  if (err) {
+    err = `${err} URL ${res || url}`;
+    if (throwOnFailure) throw err;
+    res = `data:,${err}`;
+  }
+  return res;
+}

+ 1 - 8
src/common/index.js

@@ -249,13 +249,6 @@ export function getFullUrl(url, base) {
   } catch (e) {
   } catch (e) {
     return `data:,${e.message} ${url}`;
     return `data:,${e.message} ${url}`;
   }
   }
-  // Use protocol whitelist to filter URLs
-  if (![
-    'http:',
-    'https:',
-    'ftp:',
-    'data:',
-  ].includes(obj.protocol)) obj.protocol = 'http:';
   return obj.href;
   return obj.href;
 }
 }
 
 
@@ -314,7 +307,7 @@ export function makeDataUri(raw, url) {
 
 
 /**
 /**
  * @param {VMReq.Response} response
  * @param {VMReq.Response} response
- * @returns {string}
+ * @returns {Promise<string>}
  */
  */
 export async function makeRaw(response) {
 export async function makeRaw(response) {
   const type = (response.headers.get('content-type') || '').split(';')[0] || '';
   const type = (response.headers.get('content-type') || '').split(';')[0] || '';

+ 2 - 2
src/common/limit-concurrency.js

@@ -5,8 +5,8 @@ import { makePause } from '@/common/index';
  * @param {number} max
  * @param {number} max
  * @param {number} diffKeyDelay
  * @param {number} diffKeyDelay
  * @param {number} sameKeyDelay
  * @param {number} sameKeyDelay
- * @param {function(...args): string} getKey
- * @return {function(...args): Promise}
+ * @param {function(...args: any[]): string} getKey
+ * @return {function(...args: any[]): Promise}
  */
  */
 function limitConcurrency(fn, max, diffKeyDelay, sameKeyDelay, getKey) {
 function limitConcurrency(fn, max, diffKeyDelay, sameKeyDelay, getKey) {
   const keyPromise = {};
   const keyPromise = {};

+ 4 - 7
src/confirm/views/app.vue

@@ -117,9 +117,8 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
 import Tooltip from 'vueleton/lib/tooltip';
 import Tooltip from 'vueleton/lib/tooltip';
 import Icon from '@/common/ui/icon';
 import Icon from '@/common/ui/icon';
 import {
 import {
-  debounce,
-  getFullUrl, getLocaleString, getScriptHome, i18n, isRemote,
-  makePause, makeRaw, request, sendCmdDirectly, trueJoin,
+  debounce, getFullUrl, getLocaleString, getScriptHome, i18n, isRemote, makePause, sendCmdDirectly,
+  trueJoin,
 } from '@/common';
 } from '@/common';
 import { keyboardService, modifiers } from '@/common/keyboard';
 import { keyboardService, modifiers } from '@/common/keyboard';
 import initCache from '@/common/cache';
 import initCache from '@/common/cache';
@@ -392,12 +391,10 @@ async function getFile(url, { isBlob, useCache } = {}) {
   if (useCache && cache.has(cacheKey)) {
   if (useCache && cache.has(cacheKey)) {
     return cache.get(cacheKey);
     return cache.get(cacheKey);
   }
   }
-  const response = await request(url, {
+  const { data } = await sendCmdDirectly('Request', {
+    url,
     [kResponseType]: isBlob ? 'blob' : null,
     [kResponseType]: isBlob ? 'blob' : null,
   });
   });
-  const data = isBlob
-    ? await makeRaw(response)
-    : response.data;
   if (useCache) cache.put(cacheKey, data);
   if (useCache) cache.put(cacheKey, data);
   return data;
   return data;
 }
 }