Bug 1345225 - Check if webRequest filters overlap with host permissions draft
authorTomislav Jovanovic <tomica@gmail.com>
Sun, 26 Mar 2017 20:53:28 +0200
changeset 562108 bc0f3e655bf9be7aa4ca0cf779029a29951a2b2c
parent 562107 efca7a198376e7800873e4612a8b34fc77022d4d
child 624169 e6dd96ba1afe3054cba41bfd1f759704439e5f2e
push id53952
push userbmo:tomica@gmail.com
push dateThu, 13 Apr 2017 13:34:53 +0000
bugs1345225
milestone54.0a2
Bug 1345225 - Check if webRequest filters overlap with host permissions MozReview-Commit-ID: 1tMHynv9FBO
toolkit/components/extensions/ext-webRequest.js
toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_host_permissions.html
toolkit/modules/addons/MatchPattern.jsm
toolkit/modules/tests/xpcshell/test_MatchPattern.js
--- a/toolkit/components/extensions/ext-webRequest.js
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -80,17 +80,22 @@ function WebRequestEventManager(context,
           data2[opt] = data[opt];
         }
       }
 
       return fire.sync(data2);
     };
 
     let filter2 = {};
-    filter2.urls = new MatchPattern(filter.urls);
+    if (filter.urls) {
+      filter2.urls = new MatchPattern(filter.urls);
+      if (!filter2.urls.overlapsPermissions(context.extension.whiteListedHosts)) {
+        Cu.reportError("The webRequest.addListener filter doesn't overlap with host permissions.");
+      }
+    }
     if (filter.types) {
       filter2.types = filter.types;
     }
     if (filter.tabId) {
       filter2.tabId = filter.tabId;
     }
     if (filter.windowId) {
       filter2.windowId = filter.windowId;
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_host_permissions.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_host_permissions.html
@@ -51,11 +51,36 @@ add_task(function* test_webRequest_host_
   ok((yield all.awaitMessage("png")).endsWith("great.png"), "<all_urls> permission also sees great.png");
   win2.close();
 
   yield all.unload();
   yield example.unload();
   yield mochi_test.unload();
 });
 
+add_task(async function test_webRequest_filter_permissions_warning() {
+  const manifest = {
+    permissions: ["webRequest", "http://example.com/"],
+  };
+
+  async function background() {
+    await browser.webRequest.onBeforeRequest.addListener(() => {}, {urls: ["http://example.org/"]});
+    browser.test.notifyPass();
+  }
+
+  const extension = ExtensionTestUtils.loadExtension({manifest, background});
+
+  const warning = new Promise(resolve => {
+    SimpleTest.monitorConsole(resolve, [{message: /filter doesn't overlap with host permissions/}]);
+  });
+
+  await extension.startup();
+  await extension.awaitFinish();
+
+  SimpleTest.endMonitorConsole();
+  await warning;
+
+  await extension.unload();
+});
+
 </script>
 </body>
 </html>
--- a/toolkit/modules/addons/MatchPattern.jsm
+++ b/toolkit/modules/addons/MatchPattern.jsm
@@ -95,16 +95,22 @@ SingleMatchPattern.prototype = {
     return (
       this.schemes.includes(uri.scheme) &&
       this.hostMatch(uri) &&
       (ignorePath || (
         this.pathMatch(uri.cloneIgnoringRef().path)
       ))
     );
   },
+
+  // Tests if this can possibly overlap with the |other| SingleMatchPattern.
+  overlapsIgnoringPath(other) {
+    return this.schemes.some(scheme => other.schemes.includes(scheme)) &&
+           (this.hostMatch(other) || other.hostMatch(this));
+  },
 };
 
 this.MatchPattern = function(pat) {
   this.pat = pat;
   if (!pat) {
     this.matchers = [];
   } else if (pat instanceof String || typeof(pat) == "string") {
     this.matchers = [new SingleMatchPattern(pat)];
@@ -169,16 +175,24 @@ MatchPattern.prototype = {
           return true;
         }
       }
     }
 
     return false;
   },
 
+  // Checks if every part of this filter overlaps with
+  // some of the |hosts| permissions MatchPatterns.
+  overlapsPermissions(hosts) {
+    const perms = hosts.matchers;
+    return this.matchers.length &&
+           this.matchers.every(m => perms.some(p => p.overlapsIgnoringPath(m)));
+  },
+
   serialize() {
     return this.pat;
   },
 };
 
 // Globs can match everything. Be careful, this DOES NOT filter by allowed schemes!
 this.MatchGlobs = function(globs) {
   this.original = globs;
--- a/toolkit/modules/tests/xpcshell/test_MatchPattern.js
+++ b/toolkit/modules/tests/xpcshell/test_MatchPattern.js
@@ -1,31 +1,31 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 Components.utils.import("resource://gre/modules/MatchPattern.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
-function test(url, pattern) {
-  let uri = Services.io.newURI(url);
-  let m = new MatchPattern(pattern);
-  return m.matches(uri);
-}
+function test_matches() {
+  function test(url, pattern) {
+    let uri = Services.io.newURI(url);
+    let m = new MatchPattern(pattern);
+    return m.matches(uri);
+  }
 
-function pass({url, pattern}) {
-  do_check_true(test(url, pattern), `Expected match: ${JSON.stringify(pattern)}, ${url}`);
-}
+  function pass({url, pattern}) {
+    do_check_true(test(url, pattern), `Expected match: ${JSON.stringify(pattern)}, ${url}`);
+  }
 
-function fail({url, pattern}) {
-  do_check_false(test(url, pattern), `Expected no match: ${JSON.stringify(pattern)}, ${url}`);
-}
+  function fail({url, pattern}) {
+    do_check_false(test(url, pattern), `Expected no match: ${JSON.stringify(pattern)}, ${url}`);
+  }
 
-function run_test() {
   // Invalid pattern.
   fail({url: "http://mozilla.org", pattern: ""});
 
   // Pattern must include trailing slash.
   fail({url: "http://mozilla.org", pattern: "http://mozilla.org"});
 
   // Protocol not allowed.
   fail({url: "http://mozilla.org", pattern: "gopher://wuarchive.wustl.edu/"});
@@ -84,8 +84,70 @@ function run_test() {
   pass({url: "http://mozilla.org", pattern: ["http://mozilla.org/"]});
   pass({url: "http://mozilla.org", pattern: ["http://mozilla.org/", "http://mozilla.com/"]});
   pass({url: "http://mozilla.com", pattern: ["http://mozilla.org/", "http://mozilla.com/"]});
   fail({url: "http://mozilla.biz", pattern: ["http://mozilla.org/", "http://mozilla.com/"]});
 
   // Match url with fragments.
   pass({url: "http://mozilla.org/base#some-fragment", pattern: "http://mozilla.org/base"});
 }
+
+function test_overlaps() {
+  function test(filter, hosts) {
+    const f = new MatchPattern(filter);
+    return f.overlapsPermissions(new MatchPattern(hosts));
+  }
+
+  function pass({filter = [], hosts = []}) {
+    ok(test(filter, hosts), `Expected overlap: ${filter}, ${hosts}`);
+  }
+
+  function fail({filter = [], hosts = []}) {
+    ok(!test(filter, hosts), `Expected no overlap: ${filter}, ${hosts}`);
+  }
+
+  // Direct comparison.
+  pass({hosts: "http://ab.cd/", filter: "http://ab.cd/"});
+  fail({hosts: "http://ab.cd/", filter: "ftp://ab.cd/"});
+
+  // Wildcard protocol.
+  pass({hosts: "*://ab.cd/", filter: "https://ab.cd/"});
+  fail({hosts: "*://ab.cd/", filter: "ftp://ab.cd/"});
+
+  // Wildcard subdomain.
+  pass({hosts: "http://*.ab.cd/", filter: "http://ab.cd/"});
+  pass({hosts: "http://*.ab.cd/", filter: "http://www.ab.cd/"});
+  fail({hosts: "http://*.ab.cd/", filter: "http://ab.cd.ef/"});
+  fail({hosts: "http://*.ab.cd/", filter: "http://www.cd/"});
+
+  // Wildcard subsumed.
+  pass({hosts: "http://*.ab.cd/", filter: "http://*.cd/"});
+  fail({hosts: "http://*.cd/", filter: "http://*.xy/"});
+
+  // Subdomain vs substring.
+  fail({hosts: "http://*.ab.cd/", filter: "http://fake-ab.cd/"});
+  fail({hosts: "http://*.ab.cd/", filter: "http://*.fake-ab.cd/"});
+
+  // Wildcard domain.
+  pass({hosts: "http://*/", filter: "http://ab.cd/"});
+  fail({hosts: "http://*/", filter: "https://ab.cd/"});
+
+  // Wildcard wildcards.
+  pass({hosts: "<all_urls>", filter: "ftp://ab.cd/"});
+  fail({hosts: "<all_urls>", filter: ""});
+  fail({hosts: "<all_urls>"});
+
+  // Multiple hosts.
+  pass({hosts: ["http://ab.cd/"], filter: ["http://ab.cd/"]});
+  pass({hosts: ["http://ab.cd/", "http://ab.xy/"], filter: "http://ab.cd/"});
+  pass({hosts: ["http://ab.cd/", "http://ab.xy/"], filter: "http://ab.xy/"});
+  fail({hosts: ["http://ab.cd/", "http://ab.xy/"], filter: "http://ab.zz/"});
+
+  // Multiple Multiples.
+  pass({hosts: ["http://*.ab.cd/"], filter: ["http://ab.cd/", "http://www.ab.cd/"]});
+  pass({hosts: ["http://ab.cd/", "http://ab.xy/"], filter: ["http://ab.cd/", "http://ab.xy/"]});
+  fail({hosts: ["http://ab.cd/", "http://ab.xy/"], filter: ["http://ab.cd/", "http://ab.zz/"]});
+}
+
+function run_test() {
+  test_matches();
+  test_overlaps();
+}