Bug 1287125 - Lock down mozAddonManager.install(). r=rhelmer, a=gchang
authorAndrew Swan <aswan@mozilla.com>
Fri, 15 Jul 2016 11:40:45 -0700
changeset 340172 899a01cd98bdbd6e695ed3c46c918c27ac060736
parent 340171 4468d257b7959904445ef697aa3ba59889b16fa0
child 340173 f3790db5cbcbc2084db461e050b58203de5879b4
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrhelmer, gchang
bugs1287125
milestone49.0a2
Bug 1287125 - Lock down mozAddonManager.install(). r=rhelmer, a=gchang MozReview-Commit-ID: 7wLqVme2Yzi
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/addonManager.js
toolkit/mozapps/extensions/test/browser/browser_webapi_install.js
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -41,16 +41,17 @@ const PREF_EM_HOTFIX_LASTVERSION      = 
 const PREF_EM_HOTFIX_URL              = "extensions.hotfix.url";
 const PREF_EM_CERT_CHECKATTRIBUTES    = "extensions.hotfix.cert.checkAttributes";
 const PREF_EM_HOTFIX_CERTS            = "extensions.hotfix.certs.";
 const PREF_MATCH_OS_LOCALE            = "intl.locale.matchOS";
 const PREF_SELECTED_LOCALE            = "general.useragent.locale";
 const UNKNOWN_XPCOM_ABI               = "unknownABI";
 
 const PREF_MIN_WEBEXT_PLATFORM_VERSION = "extensions.webExtensionsMinPlatformVersion";
+const PREF_WEBAPI_TESTING             = "extensions.webapi.testing";
 
 const UPDATE_REQUEST_VERSION          = 2;
 const CATEGORY_UPDATE_PARAMS          = "extension-update-params";
 
 const XMLURI_BLOCKLIST                = "http://www.mozilla.org/2006/addons-blocklist";
 
 const KEY_PROFILEDIR                  = "ProfD";
 const KEY_APPDIR                      = "XCurProcD";
@@ -61,16 +62,23 @@ const PREF_EM_CHECK_COMPATIBILITY_BASE =
 var PREF_EM_CHECK_COMPATIBILITY = MOZ_COMPATIBILITY_NIGHTLY ?
                                   PREF_EM_CHECK_COMPATIBILITY_BASE + ".nightly" :
                                   undefined;
 
 const TOOLKIT_ID                      = "toolkit@mozilla.org";
 
 const VALID_TYPES_REGEXP = /^[\w\-]+$/;
 
+const WEBAPI_INSTALL_HOSTS = ["addons.mozilla.org", "addons.cdn.mozilla.net"];
+const WEBAPI_TEST_INSTALL_HOSTS = [
+  "addons.allizom.org", "addons-stage-cdn.allizom.org",
+  "addons-dev.allizom.org", "addons-dev-cdn-allizom.org",
+  "example.com",
+];
+
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/AsyncShutdown.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
@@ -2840,17 +2848,39 @@ var AddonManagerInternal = {
       if (!info) {
         throw new Error(`forgetInstall cannot find ${id}`);
       }
       info.install.removeListener(info.listener);
       this.installs.delete(id);
     },
 
     createInstall(target, options) {
-      return new Promise((resolve) => {
+      // Throw an appropriate error if the given URL is not valid
+      // as an installation source.  Return silently if it is okay.
+      function checkInstallUrl(url) {
+        let host = Services.io.newURI(options.url, null, null).host;
+        if (WEBAPI_INSTALL_HOSTS.includes(host)) {
+          return;
+        }
+        if (Services.prefs.getBoolPref(PREF_WEBAPI_TESTING)
+            && WEBAPI_TEST_INSTALL_HOSTS.includes(host)) {
+          return;
+        }
+
+        throw new Error(`Install from ${host} not permitted`);
+      }
+
+      return new Promise((resolve, reject) => {
+        try {
+          checkInstallUrl(options.url);
+        } catch (err) {
+          reject({message: err.message});
+          return;
+        }
+
         let newInstall = install => {
           let id = this.nextInstall++;
           let listener = this.makeListener(id, target);
           install.addListener(listener);
 
           this.installs.set(id, {install, target, listener});
 
           let result = {id};
--- a/toolkit/mozapps/extensions/addonManager.js
+++ b/toolkit/mozapps/extensions/addonManager.js
@@ -242,17 +242,19 @@ amManager.prototype = {
               onInstalling: (addon, needsRestart) => handler("onInstalling", addon.id, needsRestart),
               onInstalled: (addon) => handler("onInstalled", addon.id, false),
               onUninstalling: (addon, needsRestart) => handler("onUninstalling", addon.id, needsRestart),
               onUninstalled: (addon) => handler("onUninstalled", addon.id, false),
             };
           }
           AddonManager.addAddonListener(this.addonListener);
         } else {
-          AddonManager.removeAddonListener(this.addonListener);
+          if (this.addonListener) {
+            AddonManager.removeAddonListener(this.addonListener);
+          }
         }
       }
     }
     return undefined;
   },
 
   sendEvent(target, data) {
     target.sendAsyncMessage(MSG_INSTALL_EVENT, data);
--- a/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js
@@ -1,38 +1,39 @@
 const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
 const XPI_URL = `${SECURE_TESTROOT}addons/browser_webapi_install.xpi`;
 
 const ID = "webapi_install@tests.mozilla.org";
 // eh, would be good to just stat the real file instead of this...
 const XPI_LEN = 4782;
 
-Services.prefs.setBoolPref("extensions.webapi.testing", true);
-Services.prefs.setBoolPref("extensions.install.requireBuiltInCerts", false);
-registerCleanupFunction(() => {
-  Services.prefs.clearUserPref("extensions.webapi.testing");
-  Services.prefs.clearUserPref("extensions.install.requireBuiltInCerts");
-});
-
 function waitForClear() {
   const MSG = "WebAPICleanup";
   return new Promise(resolve => {
     let listener = {
       receiveMessage: function(msg) {
         if (msg.name == MSG) {
           Services.ppmm.removeMessageListener(MSG, listener);
           resolve();
         }
       }
     };
 
     Services.ppmm.addMessageListener(MSG, listener);
   });
 }
 
+add_task(function* setup() {
+  yield SpecialPowers.pushPrefEnv({
+    set: [["extensions.webapi.testing", true],
+          ["extensions.install.requireBuiltInCerts", false]],
+  });
+  info("added preferences");
+});
+
 // Wrapper around a common task to run in the content process to test
 // the mozAddonManager API.  Takes a URL for the XPI to install and an
 // array of steps, each of which can either be an action to take
 // (i.e., start or cancel the install) or an install event to wait for.
 // Steps that look for a specific event may also include a "props" property
 // with properties that the AddonInstall object is expected to have when
 // that event is triggered.
 function* testInstall(browser, url, steps, description) {
@@ -235,8 +236,35 @@ add_task(makeInstallTest(function* (brow
   yield testInstall(browser, XPI_URL + "bogus", steps, "install of a bad url fails");
 
   let addons = yield promiseAddonsByIDs([ID]);
   is(addons[0], null, "The addon was not installed");
 
   ok(AddonManager.webAPI.installs.size > 0, "webAPI is tracking the AddonInstall");
 }));
 
+add_task(function* test_permissions() {
+  function testBadUrl(url, pattern, successMessage) {
+    return BrowserTestUtils.withNewTab(TESTPAGE, function* (browser) {
+      let result = yield ContentTask.spawn(browser, {url, pattern}, function (opts) {
+        return new Promise(resolve => {
+          content.navigator.mozAddonManager.createInstall({url: opts.url})
+            .then(() => {
+              resolve({success: false, message: "createInstall should not have succeeded"});
+            }, err => {
+              if (err.message.match(new RegExp(opts.pattern))) {
+                resolve({success: true});
+              }
+              resolve({success: false, message: `Wrong error message: ${err.message}`});
+            });
+        });
+      });
+      is(result.success, true, result.message || successMessage);
+    });
+  }
+
+  yield testBadUrl("i am not a url", "NS_ERROR_MALFORMED_URI",
+                   "Installing from an unparseable URL fails");
+
+  yield testBadUrl("https://addons.not-really-mozilla.org/impostor.xpi",
+                   "not permitted",
+                   "Installing from non-approved URL fails");
+});