Bug 1308640 bypass TP when addon has explicit permission to url, r=bz,kmag,mrbkap
authorShane Caraveo <scaraveo@mozilla.com>
Thu, 09 Feb 2017 21:08:06 -0800
changeset 341750 27d0ab29f05bc1499a95c959e5fb43fdea67e897
parent 341749 b9f44b5a9ce19fd7957e6f53c7ef3a677bde7761
child 341751 e887fb5d5d06b063e1b1085f6667da313c83d6e8
push id31342
push usercbook@mozilla.com
push dateFri, 10 Feb 2017 12:48:16 +0000
treeherdermozilla-central@b83e2b2524c9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz, kmag, mrbkap
bugs1308640
milestone54.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1308640 bypass TP when addon has explicit permission to url, r=bz,kmag,mrbkap MozReview-Commit-ID: BIhoUY2Ug8k
caps/BasePrincipal.cpp
caps/BasePrincipal.h
caps/nsIAddonPolicyService.idl
netwerk/base/nsChannelClassifier.cpp
netwerk/base/nsChannelClassifier.h
toolkit/components/extensions/ExtensionManagement.jsm
toolkit/components/extensions/test/mochitest/chrome.ini
toolkit/components/extensions/test/mochitest/test_chrome_ext_trackingprotection.html
toolkit/components/utils/simpleServices.js
toolkit/modules/addons/MatchPattern.jsm
--- a/caps/BasePrincipal.cpp
+++ b/caps/BasePrincipal.cpp
@@ -725,23 +725,23 @@ BasePrincipal::CloneStrippingUserContext
   nsCOMPtr<nsIURI> uri;
   rv = NS_NewURI(getter_AddRefs(uri), originNoSuffix);
   NS_ENSURE_SUCCESS(rv, nullptr);
 
   return BasePrincipal::CreateCodebasePrincipal(uri, attrs);
 }
 
 bool
-BasePrincipal::AddonAllowsLoad(nsIURI* aURI)
+BasePrincipal::AddonAllowsLoad(nsIURI* aURI, bool aExplicit /* = false */)
 {
   if (mOriginAttributes.mAddonId.IsEmpty()) {
     return false;
   }
 
   nsCOMPtr<nsIAddonPolicyService> aps = do_GetService("@mozilla.org/addons/policy-service;1");
   NS_ENSURE_TRUE(aps, false);
 
   bool allowed = false;
-  nsresult rv = aps->AddonMayLoadURI(mOriginAttributes.mAddonId, aURI, &allowed);
+  nsresult rv = aps->AddonMayLoadURI(mOriginAttributes.mAddonId, aURI, aExplicit, &allowed);
   return NS_SUCCEEDED(rv) && allowed;
 }
 
 } // namespace mozilla
--- a/caps/BasePrincipal.h
+++ b/caps/BasePrincipal.h
@@ -259,34 +259,35 @@ public:
     eExpandedPrincipal,
     eSystemPrincipal
   };
 
   virtual PrincipalKind Kind() = 0;
 
   already_AddRefed<BasePrincipal> CloneStrippingUserContextIdAndFirstPartyDomain();
 
+  // Helper to check whether this principal is associated with an addon that
+  // allows unprivileged code to load aURI.  aExplicit == true will prevent
+  // use of all_urls permission, requiring the domain in its permissions.
+  bool AddonAllowsLoad(nsIURI* aURI, bool aExplicit = false);
+
 protected:
   virtual ~BasePrincipal();
 
   virtual nsresult GetOriginInternal(nsACString& aOrigin) = 0;
   // Note that this does not check OriginAttributes. Callers that depend on
   // those must call Subsumes instead.
   virtual bool SubsumesInternal(nsIPrincipal* aOther, DocumentDomainConsideration aConsider) = 0;
 
   // Internal, side-effect-free check to determine whether the concrete
   // principal would allow the load ignoring any common behavior implemented in
   // BasePrincipal::CheckMayLoad.
   virtual bool MayLoadInternal(nsIURI* aURI) = 0;
   friend class ::nsExpandedPrincipal;
 
-  // Helper to check whether this principal is associated with an addon that
-  // allows unprivileged code to load aURI.
-  bool AddonAllowsLoad(nsIURI* aURI);
-
   nsCOMPtr<nsIContentSecurityPolicy> mCSP;
   nsCOMPtr<nsIContentSecurityPolicy> mPreloadCSP;
   OriginAttributes mOriginAttributes;
 };
 
 } // namespace mozilla
 
 #endif /* mozilla_BasePrincipal_h */
--- a/caps/nsIAddonPolicyService.idl
+++ b/caps/nsIAddonPolicyService.idl
@@ -42,19 +42,20 @@ interface nsIAddonPolicyService : nsISup
 
   /**
    * Returns true if the addon was granted the |aPerm| API permission.
    */
   boolean addonHasPermission(in AString aAddonId, in AString aPerm);
 
   /**
    * Returns true if unprivileged code associated with the given addon may load
-   * data from |aURI|.
+   * data from |aURI|.  If |aExplicit| is true, the <all_urls> permission and
+   * permissive host globs are ignored when checking for a match.
    */
-  boolean addonMayLoadURI(in AString aAddonId, in nsIURI aURI);
+  boolean addonMayLoadURI(in AString aAddonId, in nsIURI aURI, [optional] in boolean aExplicit);
 
   /**
    * Returns true if a given extension:// URI is web-accessible.
    */
   boolean extensionURILoadableByAnyone(in nsIURI aURI);
 
   /**
    * Maps an extension URI to the ID of the addon it belongs to.
--- a/netwerk/base/nsChannelClassifier.cpp
+++ b/netwerk/base/nsChannelClassifier.cpp
@@ -4,16 +4,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsChannelClassifier.h"
 
 #include "mozIThirdPartyUtil.h"
 #include "nsCharSeparatedTokenizer.h"
 #include "nsContentUtils.h"
+#include "nsIAddonPolicyService.h"
 #include "nsICacheEntry.h"
 #include "nsICachingChannel.h"
 #include "nsIChannel.h"
 #include "nsIDocShell.h"
 #include "nsIDocument.h"
 #include "nsIDOMDocument.h"
 #include "nsIHttpChannel.h"
 #include "nsIHttpChannelInternal.h"
@@ -143,16 +144,20 @@ nsChannelClassifier::ShouldEnableTrackin
       if (LOG_ENABLED()) {
         LOG(("nsChannelClassifier[%p]: Skipping tracking protection checks "
              "for first party or top-level load channel[%p] with uri %s",
              this, aChannel, chanURI->GetSpecOrDefault().get()));
       }
       return NS_OK;
     }
 
+    if (AddonMayLoad(aChannel, chanURI)) {
+        return NS_OK;
+    }
+
     nsCOMPtr<nsIIOService> ios = do_GetService(NS_IOSERVICE_CONTRACTID, &rv);
     NS_ENSURE_SUCCESS(rv, rv);
 
     const char ALLOWLIST_EXAMPLE_PREF[] = "channelclassifier.allowlist_example";
     if (!topWinURI && Preferences::GetBool(ALLOWLIST_EXAMPLE_PREF, false)) {
       LOG(("nsChannelClassifier[%p]: Allowlisting test domain\n", this));
       rv = ios->NewURI(NS_LITERAL_CSTRING("http://allowlisted.example.com"),
                        nullptr, nullptr, getter_AddRefs(topWinURI));
@@ -228,16 +233,33 @@ nsChannelClassifier::ShouldEnableTrackin
 
     // Tracking protection will be disabled so update the security state
     // of the document and fire a secure change event. If we can't get the
     // window for the channel, then the shield won't show up so we can't send
     // an event to the securityUI anyway.
     return NotifyTrackingProtectionDisabled(aChannel);
 }
 
+bool
+nsChannelClassifier::AddonMayLoad(nsIChannel *aChannel, nsIURI *aUri)
+{
+    nsCOMPtr<nsILoadInfo> channelLoadInfo = aChannel->GetLoadInfo();
+    if (!channelLoadInfo)
+        return false;
+
+    // loadingPrincipal is used here to ensure we are loading into an
+    // addon principal.  This allows an addon, with explicit permission, to
+    // call out to API endpoints that may otherwise get blocked.
+    nsIPrincipal* loadingPrincipal = channelLoadInfo->LoadingPrincipal();
+    if (!loadingPrincipal)
+        return false;
+
+    return BasePrincipal::Cast(loadingPrincipal)->AddonAllowsLoad(aUri, true);
+}
+
 // static
 nsresult
 nsChannelClassifier::NotifyTrackingProtectionDisabled(nsIChannel *aChannel)
 {
     // Can be called in EITHER the parent or child process.
     nsCOMPtr<nsIParentChannel> parentChannel;
     NS_QueryNotificationCallbacks(aChannel, parentChannel);
     if (parentChannel) {
--- a/netwerk/base/nsChannelClassifier.h
+++ b/netwerk/base/nsChannelClassifier.h
@@ -54,16 +54,17 @@ private:
     // Helper function to check a URI against the hostname whitelist
     bool IsHostnameWhitelisted(nsIURI *aUri, const nsACString &aWhitelisted);
     // Checks that the channel was loaded by the URI currently loaded in aDoc
     static bool SameLoadingURI(nsIDocument *aDoc, nsIChannel *aChannel);
 
     nsresult ShouldEnableTrackingProtectionInternal(nsIChannel *aChannel,
                                                     bool *result);
 
+    bool AddonMayLoad(nsIChannel *aChannel, nsIURI *aUri);
 public:
     // If we are blocking tracking content, update the corresponding flag in
     // the respective docshell and call nsISecurityEventSink::onSecurityChange.
     static nsresult SetBlockedTrackingContent(nsIChannel *channel);
     static nsresult NotifyTrackingProtectionDisabled(nsIChannel *aChannel);
 };
 
 } // namespace net
--- a/toolkit/components/extensions/ExtensionManagement.jsm
+++ b/toolkit/components/extensions/ExtensionManagement.jsm
@@ -202,18 +202,18 @@ var Service = {
       path = path.substr(1);
     }
     return extension.webAccessibleResources.matches(path);
   },
 
   // Checks whether a given extension can load this URI (typically via
   // an XML HTTP request). The manifest.json |permissions| directive
   // determines this.
-  checkAddonMayLoad(extension, uri) {
-    return extension.whiteListedHosts.matchesIgnoringPath(uri);
+  checkAddonMayLoad(extension, uri, explicit = false) {
+    return extension.whiteListedHosts.matchesIgnoringPath(uri, explicit);
   },
 
   generateBackgroundPageUrl(extension) {
     let background_scripts = (extension.manifest.background &&
                               extension.manifest.background.scripts);
 
     if (!background_scripts) {
       return;
--- a/toolkit/components/extensions/test/mochitest/chrome.ini
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -29,8 +29,9 @@ skip-if = os != "mac" && os != "linux"
 [test_ext_jsversion.html]
 [test_ext_schema.html]
 [test_chrome_ext_storage_cleanup.html]
 [test_chrome_ext_idle.html]
 [test_chrome_ext_identity.html]
 skip-if = os == 'android' # unsupported.
 [test_chrome_ext_downloads_saveAs.html]
 [test_chrome_ext_webrequest_background_events.html]
+[test_chrome_ext_trackingprotection.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_trackingprotection.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for simple WebExtension</title>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="chrome_head.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+var {UrlClassifierTestUtils} = Cu.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
+
+function tp_background(expectFail = true) {
+  fetch("https://tracking.example.com/example.txt").then(() => {
+    browser.test.assertTrue(!expectFail, "fetch received");
+    browser.test.sendMessage("done");
+  }, () => {
+    browser.test.assertTrue(expectFail, "fetch failure");
+    browser.test.sendMessage("done");
+  });
+}
+
+function* test_permission(permissions, expectFail) {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions,
+    },
+    background: `(${tp_background})(${expectFail})`,
+  });
+
+  yield extension.startup();
+  yield extension.awaitMessage("done");
+  yield extension.unload();
+}
+
+add_task(function* setup() {
+  yield UrlClassifierTestUtils.addTestTrackers();
+  yield SpecialPowers.pushPrefEnv({
+    set: [["privacy.trackingprotection.enabled", true]],
+  });
+});
+
+// Fetch would be blocked with these tests
+add_task(function* () { yield test_permission([], true); });
+add_task(function* () { yield test_permission(["http://*/"], true); });
+add_task(function* () { yield test_permission(["http://*.example.com/"], true); });
+add_task(function* () { yield test_permission(["http://localhost/*"], true); });
+add_task(function* () { yield test_permission(["<all_urls>"], true); });
+// Fetch will not be blocked with explicit permissions
+add_task(function* () { yield test_permission(["*://tracking.example.com/*"], false); });
+
+add_task(function* test_contentscript() {
+  function contentScript() {
+    fetch("https://tracking.example.com/example.txt").then(() => {
+      browser.test.notifyPass("fetch received");
+    }, () => {
+      browser.test.notifyFail("fetch failure");
+    });
+  }
+
+  let extensionData = {
+    manifest: {
+      permissions: ["*://tracking.example.com/*"],
+      content_scripts: [
+        {
+          "matches": ["http://mochi.test/*/file_sample.html"],
+          "js": ["content_script.js"],
+          "run_at": "document_start",
+        },
+      ],
+    },
+
+    files: {
+      "content_script.js": contentScript,
+    },
+  };
+  const url = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest/file_sample.html";
+
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+  yield extension.startup();
+  let win = window.open(url);
+  yield extension.awaitFinish();
+  win.close();
+  yield extension.unload();
+});
+
+add_task(function* teardown() {
+  UrlClassifierTestUtils.cleanupTestTrackers();
+});
+</script>
+
+</body>
+</html>
--- a/toolkit/components/utils/simpleServices.js
+++ b/toolkit/components/utils/simpleServices.js
@@ -79,19 +79,19 @@ AddonPolicyService.prototype = {
 
   /*
    * Invokes a callback (if any) associated with the addon to determine whether
    * unprivileged code running within the addon is allowed to perform loads from
    * the given URI.
    *
    * @see nsIAddonPolicyService.addonMayLoadURI
    */
-  addonMayLoadURI(aAddonId, aURI) {
+  addonMayLoadURI(aAddonId, aURI, aExplicit = false) {
     let cb = this.mayLoadURICallbacks.get(aAddonId);
-    return cb ? cb(aURI) : false;
+    return cb ? cb(aURI, aExplicit) : false;
   },
 
   /*
    * Invokes a callback (if any) associated with the addon to loclaize a
    * resource belonging to that add-on.
    */
   localizeAddonString(aAddonId, aString) {
     let cb = this.localizeCallbacks.get(aAddonId);
--- a/toolkit/modules/addons/MatchPattern.jsm
+++ b/toolkit/modules/addons/MatchPattern.jsm
@@ -34,16 +34,17 @@ function globToRegexp(pat, allowQuestion
   }
   pat = pat.replace(/\*/g, ".*");
   return new RegExp("^" + pat + "$");
 }
 
 // These patterns follow the syntax in
 // https://developer.chrome.com/extensions/match_patterns
 function SingleMatchPattern(pat) {
+  this.pat = pat;
   if (pat == "<all_urls>") {
     this.schemes = PERMITTED_SCHEMES;
     this.hostMatch = () => true;
     this.pathMatch = () => true;
   } else if (!pat) {
     this.schemes = [];
   } else {
     let re = new RegExp(`^(${PERMITTED_SCHEMES_REGEXP}|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+|)(/.*)$`);
@@ -105,25 +106,34 @@ this.MatchPattern = function(pat) {
   this.pat = pat;
   if (!pat) {
     this.matchers = [];
   } else if (pat instanceof String || typeof(pat) == "string") {
     this.matchers = [new SingleMatchPattern(pat)];
   } else {
     this.matchers = pat.map(p => new SingleMatchPattern(p));
   }
+
+  XPCOMUtils.defineLazyGetter(this, "explicitMatchers", () => {
+    return this.matchers.filter(matcher => matcher.pat != "<all_urls>" &&
+                                           matcher.host &&
+                                           !matcher.host.startsWith("*"));
+  });
 };
 
 MatchPattern.prototype = {
   // |uri| should be an nsIURI.
   matches(uri) {
     return this.matchers.some(matcher => matcher.matches(uri));
   },
 
-  matchesIgnoringPath(uri) {
+  matchesIgnoringPath(uri, explicit = false) {
+    if (explicit) {
+      return this.explicitMatchers.some(matcher => matcher.matches(uri, true));
+    }
     return this.matchers.some(matcher => matcher.matches(uri, true));
   },
 
   // Checks that this match pattern grants access to read the given
   // cookie. |cookie| should be an |nsICookie2| instance.
   matchesCookie(cookie) {
     // First check for simple matches.
     let secureURI = NetUtil.newURI(`https://${cookie.rawHost}/`);