bug 1287125 Lock down mozAddonManager.install() r=rhelmer
authorAndrew Swan <aswan@mozilla.com>
Fri, 15 Jul 2016 11:40:45 -0700
changeset 306056 e25f1a122d9dc433d907ec2d1a6c9108cb3fbec0
parent 306055 8d4f43219dcfeb6b102f974e2617a31c1745a989
child 306057 70a2074086327156d960bdcb84d5606ca9323ad8
push id79765
push usercbook@mozilla.com
push dateThu, 21 Jul 2016 14:26:34 +0000
treeherdermozilla-inbound@ab54bfc55266 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrhelmer
bugs1287125
milestone50.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 1287125 Lock down mozAddonManager.install() r=rhelmer 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");
@@ -2891,17 +2899,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
@@ -243,17 +243,19 @@ amManager.prototype = {
               onInstalled: (addon) => handler("onInstalled", addon.id, false),
               onUninstalling: (addon, needsRestart) => handler("onUninstalling", addon.id, needsRestart),
               onUninstalled: (addon) => handler("onUninstalled", addon.id, false),
               onOperationCancelled: (addon) => handler("onOperationCancelled", 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");
+});