Bug 1042699: Block cross-origin add-on install requests. r=dveditz
authorDave Townsend <dtownsend@oxymoronical.com>
Tue, 18 Aug 2015 17:21:05 -0700
changeset 259141 9d2d9b4a95b9c42738a63308a72b19b65b0723ec
parent 259140 5b6fdfc822e9d18614035d0ce4962ac63eb842b6
child 259142 979bb826f0407331442a7b6cab57ebb6d015694b
push id29269
push userryanvm@gmail.com
push dateTue, 25 Aug 2015 00:57:59 +0000
treeherdermozilla-central@04b8c412d9f5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdveditz
bugs1042699
milestone43.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 1042699: Block cross-origin add-on install requests. r=dveditz
browser/base/content/browser-addons.js
browser/base/content/browser.js
browser/base/content/test/general/browser_bug553455.js
browser/themes/shared/notification-icons.inc.css
caps/nsIScriptSecurityManager.idl
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/addonManager.js
toolkit/mozapps/extensions/amContentHandler.js
toolkit/mozapps/extensions/amIWebInstallListener.idl
toolkit/mozapps/extensions/amInstallTrigger.js
toolkit/mozapps/extensions/amWebInstallListener.js
toolkit/mozapps/extensions/content/extensions.js
toolkit/mozapps/extensions/content/xpinstallConfirm.js
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/test/xpcshell/test_blocklistchange.js
toolkit/mozapps/extensions/test/xpcshell/test_permissions.js
toolkit/mozapps/extensions/test/xpcshell/test_permissions_prefs.js
toolkit/mozapps/extensions/test/xpinstall/browser.ini
toolkit/mozapps/extensions/test/xpinstall/browser_datauri.js
toolkit/mozapps/extensions/test/xpinstall/browser_httphash6.js
toolkit/mozapps/extensions/test/xpinstall/browser_localfile.js
toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js
toolkit/mozapps/extensions/test/xpinstall/browser_multipackage.js
toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway.js
toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway2.js
toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway3.js
toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway4.js
toolkit/mozapps/extensions/test/xpinstall/browser_signed_url.js
toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_iframe.js
toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_xorigin.js
toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_url.js
toolkit/mozapps/extensions/test/xpinstall/browser_whitelist3.js
toolkit/mozapps/extensions/test/xpinstall/browser_whitelist4.js
toolkit/mozapps/extensions/test/xpinstall/browser_whitelist7.js
toolkit/mozapps/extensions/test/xpinstall/head.js
toolkit/mozapps/extensions/test/xpinstall/installtrigger_frame.html
toolkit/mozapps/extensions/test/xpinstall/navigate.html
toolkit/mozapps/extensions/test/xpinstall/redirect.sjs
--- a/browser/base/content/browser-addons.js
+++ b/browser/base/content/browser-addons.js
@@ -1,13 +1,39 @@
 # -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
 # This Source Code Form is subject to the terms of the Mozilla Public
 # 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/.
 
+// Removes a doorhanger notification if all of the installs it was notifying
+// about have ended in some way.
+function removeNotificationOnEnd(notification, installs) {
+  let count = installs.length;
+
+  function maybeRemove(install) {
+    install.removeListener(this);
+
+    if (--count == 0) {
+      // Check that the notification is still showing
+      let current = PopupNotifications.getNotification(notification.id, notification.browser);
+      if (current === notification)
+        notification.remove();
+    }
+  }
+
+  for (let install of installs) {
+    install.addListener({
+      onDownloadCancelled: maybeRemove,
+      onDownloadFailed: maybeRemove,
+      onInstallFailed: maybeRemove,
+      onInstallEnded: maybeRemove
+    });
+  }
+}
+
 const gXPInstallObserver = {
   _findChildShell: function (aDocShell, aSoughtShell)
   {
     if (aDocShell == aSoughtShell)
       return aDocShell;
 
     var node = aDocShell.QueryInterface(Components.interfaces.nsIDocShellTreeItem);
     for (var i = 0; i < node.childCount; ++i) {
@@ -38,45 +64,55 @@ const gXPInstallObserver = {
       if (pending) {
         pending.push(installInfo);
       } else {
         this.pendingInstalls.set(browser, [installInfo]);
       }
       return;
     }
 
+    let showNextConfirmation = () => {
+      // Make sure the browser is still alive.
+      if (gBrowser.browsers.indexOf(browser) == -1)
+        return;
+
+      let pending = this.pendingInstalls.get(browser);
+      if (pending && pending.length)
+        this.showInstallConfirmation(browser, pending.shift());
+    }
+
+    // If all installs have already been cancelled in some way then just show
+    // the next confirmation
+    if (installInfo.installs.every(i => i.state != AddonManager.STATE_DOWNLOADED)) {
+      showNextConfirmation();
+      return;
+    }
+
     const anchorID = "addons-notification-icon";
 
     // Make notifications persist a minimum of 30 seconds
     var options = {
       displayURI: installInfo.originatingURI,
       timeout: Date.now() + 30000,
     };
 
     let cancelInstallation = () => {
       if (installInfo) {
-        for (let install of installInfo.installs)
-          install.cancel();
+        for (let install of installInfo.installs) {
+          // The notification may have been closed because the add-ons got
+          // cancelled elsewhere, only try to cancel those that are still
+          // pending install.
+          if (install.state != AddonManager.STATE_CANCELLED)
+            install.cancel();
+        }
       }
 
       this.acceptInstallation = null;
 
-      let tab = gBrowser.getTabForBrowser(browser);
-      if (tab)
-        tab.removeEventListener("TabClose", cancelInstallation);
-
-      window.removeEventListener("unload", cancelInstallation);
-
-      // Make sure the browser is still alive.
-      if (gBrowser.browsers.indexOf(browser) == -1)
-        return;
-
-      let pending = this.pendingInstalls.get(browser);
-      if (pending && pending.length)
-        this.showInstallConfirmation(browser, pending.shift());
+      showNextConfirmation();
     };
 
     let unsigned = installInfo.installs.filter(i => i.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING);
     let someUnsigned = unsigned.length > 0 && unsigned.length < installInfo.installs.length;
 
     options.eventCallback = (aEvent) => {
       switch (aEvent) {
         case "removed":
@@ -160,23 +196,23 @@ const gXPInstallObserver = {
     if (height) {
       let notification = document.getElementById("addon-install-confirmation-notification");
       notification.style.minHeight = height + "px";
     }
 
     let tab = gBrowser.getTabForBrowser(browser);
     if (tab) {
       gBrowser.selectedTab = tab;
-      tab.addEventListener("TabClose", cancelInstallation);
     }
 
-    window.addEventListener("unload", cancelInstallation);
+    let popup = PopupNotifications.show(browser, "addon-install-confirmation",
+                                        messageString, anchorID, null, null,
+                                        options);
 
-    PopupNotifications.show(browser, "addon-install-confirmation", messageString,
-                            anchorID, null, null, options);
+    removeNotificationOnEnd(popup, installInfo.installs);
 
     Services.telemetry
             .getHistogramById("SECURITY_UI")
             .add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL);
   },
 
   observe: function (aSubject, aTopic, aData)
   {
@@ -217,33 +253,46 @@ const gXPInstallObserver = {
             gPrefService.setBoolPref("xpinstall.enabled", true);
           }
         };
       }
 
       PopupNotifications.show(browser, notificationID, messageString, anchorID,
                               action, null, options);
       break; }
+    case "addon-install-origin-blocked": {
+      messageString = gNavigatorBundle.getFormattedString("xpinstallPromptMessage",
+                        [brandShortName]);
+
+      let secHistogram = Components.classes["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry).getHistogramById("SECURITY_UI");
+      secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED);
+      let popup = PopupNotifications.show(browser, notificationID,
+                                          messageString, anchorID,
+                                          null, null, options);
+      removeNotificationOnEnd(popup, installInfo.installs);
+      break; }
     case "addon-install-blocked": {
       messageString = gNavigatorBundle.getFormattedString("xpinstallPromptMessage",
                         [brandShortName]);
 
       let secHistogram = Components.classes["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry).getHistogramById("SECURITY_UI");
       action = {
         label: gNavigatorBundle.getString("xpinstallPromptAllowButton"),
         accessKey: gNavigatorBundle.getString("xpinstallPromptAllowButton.accesskey"),
         callback: function() {
           secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH);
           installInfo.install();
         }
       };
 
       secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED);
-      PopupNotifications.show(browser, notificationID, messageString, anchorID,
-                              action, null, options);
+      let popup = PopupNotifications.show(browser, notificationID,
+                                          messageString, anchorID,
+                                          action, null, options);
+      removeNotificationOnEnd(popup, installInfo.installs);
       break; }
     case "addon-install-started": {
       let needsDownload = function needsDownload(aInstall) {
         return aInstall.state != AddonManager.STATE_DOWNLOADED;
       }
       // If all installs have already been downloaded then there is no need to
       // show the download progress
       if (!installInfo.installs.some(needsDownload))
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1248,16 +1248,17 @@ var gBrowserInit = {
     // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008.
     setTimeout(function() { SafeBrowsing.init(); }, 2000);
 #endif
 
     Services.obs.addObserver(gSessionHistoryObserver, "browser:purge-session-history", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-disabled", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-started", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-blocked", false);
+    Services.obs.addObserver(gXPInstallObserver, "addon-install-origin-blocked", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-failed", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-confirmation", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-complete", false);
     window.messageManager.addMessageListener("Browser:URIFixup", gKeywordURIFixup);
 
     BrowserOffline.init();
     OfflineApps.init();
     IndexedDBPromptHelper.init();
@@ -1564,16 +1565,17 @@ var gBrowserInit = {
       gBrowserThumbnails.uninit();
       LoopUI.uninit();
       FullZoom.destroy();
 
       Services.obs.removeObserver(gSessionHistoryObserver, "browser:purge-session-history");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-disabled");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-started");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-blocked");
+      Services.obs.removeObserver(gXPInstallObserver, "addon-install-origin-blocked");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-failed");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-confirmation");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-complete");
       window.messageManager.removeMessageListener("Browser:URIFixup", gKeywordURIFixup);
       window.messageManager.removeMessageListener("Browser:LoadURI", RedirectLoad);
 
       try {
         gPrefService.removeObserver(gHomeButton.prefDomain, gHomeButton);
--- a/browser/base/content/test/general/browser_bug553455.js
+++ b/browser/base/content/test/general/browser_bug553455.js
@@ -189,18 +189,17 @@ function test_disabled_install() {
       }
       catch (e) {
         ok(false, "xpinstall.enabled should be set");
       }
 
       gBrowser.removeTab(gBrowser.selectedTab);
 
       AddonManager.getAllInstalls(function(aInstalls) {
-        is(aInstalls.length, 1, "Should have been one install created");
-        aInstalls[0].cancel();
+        is(aInstalls.length, 0, "Shouldn't be any pending installs");
 
         runNextTest();
       });
     });
 
     // Click on Enable
     EventUtils.synthesizeMouseAtCenter(notification.button, {});
   });
@@ -669,18 +668,20 @@ function test_url() {
           gBrowser.removeTab(gBrowser.selectedTab);
         });
       });
 
       accept_install_dialog();
     });
   });
 
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.loadURI(TESTROOT + "unsigned.xpi");
+  gBrowser.selectedTab = gBrowser.addTab("about:blank");
+  BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+    gBrowser.loadURI(TESTROOT + "unsigned.xpi");
+  });
 },
 
 function test_localfile() {
   // Wait for the install to fail
   Services.obs.addObserver(function() {
     Services.obs.removeObserver(arguments.callee, "addon-install-failed");
 
     // Wait for the browser code to add the failure notification
@@ -698,18 +699,20 @@ function test_localfile() {
 
   var cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
                      .getService(Components.interfaces.nsIChromeRegistry);
   try {
     var path = cr.convertChromeURL(makeURI(CHROMEROOT + "corrupt.xpi")).spec;
   } catch (ex) {
     var path = CHROMEROOT + "corrupt.xpi";
   }
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.loadURI(path);
+  gBrowser.selectedTab = gBrowser.addTab("about:blank");
+  BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+    gBrowser.loadURI(path);
+  });
 },
 
 function test_tabclose() {
   if (!Preferences.get("xpinstall.customConfirmationUI", false)) {
     runNextTest();
     return;
   }
 
@@ -727,18 +730,80 @@ function test_tabclose() {
           });
         });
 
         gBrowser.removeTab(gBrowser.selectedTab);
       });
     });
   });
 
+  gBrowser.selectedTab = gBrowser.addTab("about:blank");
+  BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+    gBrowser.loadURI(TESTROOT + "unsigned.xpi");
+  });
+},
+
+// Add-ons should be cancelled and the install notification destroyed when
+// navigating to a new origin
+function test_tabnavigate() {
+  if (!Preferences.get("xpinstall.customConfirmationUI", false)) {
+    runNextTest();
+    return;
+  }
+
+  // Wait for the progress notification
+  wait_for_progress_notification(aPanel => {
+    // Wait for the install confirmation dialog
+    wait_for_install_dialog(() => {
+      wait_for_notification_close(() => {
+        AddonManager.getAllInstalls(aInstalls => {
+          is(aInstalls.length, 0, "Should be no pending install");
+
+          Services.perms.remove(makeURI("http://example.com/"), "install");
+          loadPromise.then(() => {
+            gBrowser.removeTab(gBrowser.selectedTab);
+            runNextTest();
+          });
+        });
+      });
+
+      let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+      gBrowser.loadURI("about:blank");
+    });
+  });
+
+  var pm = Services.perms;
+  pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+  var triggers = encodeURIComponent(JSON.stringify({
+    "Extension XPI": "unsigned.xpi"
+  }));
   gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.loadURI(TESTROOT + "unsigned.xpi");
+  gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+},
+
+function test_urlbar() {
+  wait_for_notification("addon-install-origin-blocked", function(aPanel) {
+    let notification = aPanel.childNodes[0];
+
+    is(notification.button.label, "", "Button to allow install should be hidden.");
+
+    wait_for_notification_close(() => {
+      runNextTest();
+    });
+
+    gBrowser.removeCurrentTab();
+  });
+
+  gBrowser.selectedTab = gBrowser.addTab("about:blank");
+  BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+    gURLBar.value = TESTROOT + "unsigned.xpi";
+    gURLBar.focus();
+    EventUtils.synthesizeKey("VK_RETURN", {});
+  });
 },
 
 function test_wronghost() {
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.addEventListener("load", function() {
     if (gBrowser.currentURI.spec != TESTROOT2 + "enabled.html")
       return;
 
@@ -865,22 +930,26 @@ function test_renotify_blocked() {
   wait_for_notification("addon-install-blocked", function(aPanel) {
     let notification = aPanel.childNodes[0];
 
     wait_for_notification_close(function () {
       info("Timeouts after this probably mean bug 589954 regressed");
       executeSoon(function () {
         wait_for_notification("addon-install-blocked", function(aPanel) {
           AddonManager.getAllInstalls(function(aInstalls) {
-          is(aInstalls.length, 2, "Should be two pending installs");
-            aInstalls[0].cancel();
-            aInstalls[1].cancel();
+            is(aInstalls.length, 2, "Should be two pending installs");
+
+            wait_for_notification_close(() => {
+              AddonManager.getAllInstalls(function(aInstalls) {
+                is(aInstalls.length, 0, "Should have cancelled the installs");
+                runNextTest();
+              });
+            });
 
             info("Closing browser tab");
-            wait_for_notification_close(runNextTest);
             gBrowser.removeTab(gBrowser.selectedTab);
           });
         });
 
         gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
       });
     });
 
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -14,17 +14,18 @@
   list-style-image: url(chrome://browser/skin/Geolocation-64.png);
 }
 
 .popup-notification-icon[popupid="push"] {
   list-style-image: url(chrome://browser/skin/Push-64.png);
 }
 
 .popup-notification-icon[popupid="xpinstall-disabled"],
-.popup-notification-icon[popupid="addon-install-blocked"] {
+.popup-notification-icon[popupid="addon-install-blocked"],
+.popup-notification-icon[popupid="addon-install-origin-blocked"] {
   list-style-image: url(chrome://browser/skin/addons/addon-install-blocked.svg);
 }
 
 .popup-notification-icon[popupid="addon-progress"] {
   list-style-image: url(chrome://browser/skin/addons/addon-install-downloading.svg);
 }
 
 .popup-notification-icon[popupid="addon-install-failed"] {
--- a/caps/nsIScriptSecurityManager.idl
+++ b/caps/nsIScriptSecurityManager.idl
@@ -182,26 +182,26 @@ interface nsIScriptSecurityManager : nsI
      * Legacy method for getting a principal with no origin attributes.
      *
      * @deprecated use createCodebasePrincipal instead.
      */
     [deprecated] nsIPrincipal getCodebasePrincipal(in nsIURI uri);
 
     /**
      * Returns a principal whose origin is composed of |uri| and |originAttributes|.
-     * See nsIPrincipal.h for a description of origin attributes, and
-     * SystemDictionaries.webidl for a list of origin attributes and their defaults.
+     * See nsIPrincipal.idl for a description of origin attributes, and
+     * ChromeUtils.webidl for a list of origin attributes and their defaults.
      */
     [implicit_jscontext]
     nsIPrincipal createCodebasePrincipal(in nsIURI uri, in jsval originAttributes);
 
     /**
      * Returns a unique nonce principal with |originAttributes|.
-     * See nsIPrincipal.h for a description of origin attributes, and
-     * SystemDictionaries.webidl for a list of origin attributes and their defaults.
+     * See nsIPrincipal.idl for a description of origin attributes, and
+     * ChromeUtils.webidl for a list of origin attributes and their defaults.
      */
     [implicit_jscontext]
     nsIPrincipal createNullPrincipal(in jsval originAttributes);
 
     /**
      * Creates an expanded principal whose capabilities are the union of the
      * given principals. An expanded principal has an asymmetric privilege
      * relationship with its sub-principals (that is to say, it subsumes the
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -299,16 +299,47 @@ function getLocale() {
     return Services.prefs.getCharPref(PREF_SELECTED_LOCALE);
   }
   catch (e) { }
 
   return "en-US";
 }
 
 /**
+ * Previously the APIs for installing add-ons from webpages accepted nsIURI
+ * arguments for the installing page. They now take an nsIPrincipal but for now
+ * maintain backwards compatibility by converting an nsIURI to an nsIPrincipal.
+ *
+ * @param  aPrincipalOrURI
+ *         The argument passed to the API function. Can be null, an nsIURI or
+ *         an nsIPrincipal.
+ * @return an nsIPrincipal.
+ */
+function ensurePrincipal(principalOrURI) {
+  if (principalOrURI instanceof Ci.nsIPrincipal)
+    return principalOrURI;
+
+  logger.warn("Deprecated API call, please pass a non-null nsIPrincipal instead of an nsIURI");
+
+  // Previously a null installing URI meant allowing the install regardless.
+  if (!principalOrURI) {
+    return Services.scriptSecurityManager.getSystemPrincipal();
+  }
+
+  if (principalOrURI instanceof Ci.nsIURI) {
+    return Services.scriptSecurityManager.createCodebasePrincipal(principalOrURI, {
+      inBrowser: true
+    });
+  }
+
+  // Just return whatever we have, the API method will log an error about it.
+  return principalOrURI;
+}
+
+/**
  * A helper class to repeatedly call a listener with each object in an array
  * optionally checking whether the object has a method in it.
  *
  * @param  aObjects
  *         The array of objects to iterate through
  * @param  aMethod
  *         An optional method name, if not null any objects without this method
  *         will not be passed to the listener
@@ -345,16 +376,110 @@ AsyncObjectCaller.prototype = {
     if (!this.method || this.method in object)
       this.listener.nextObject(this, object);
     else
       this.callNext();
   }
 };
 
 /**
+ * Listens for a browser changing origin and cancels the installs that were
+ * started by it.
+ */
+function BrowserListener(aBrowser, aInstallingPrincipal, aInstalls) {
+  this.browser = aBrowser;
+  this.principal = aInstallingPrincipal;
+  this.installs = aInstalls;
+  this.installCount = aInstalls.length;
+
+  aBrowser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
+  Services.obs.addObserver(this, "message-manager-close", true);
+
+  for (let install of this.installs)
+    install.addListener(this);
+
+  this.registered = true;
+}
+
+BrowserListener.prototype = {
+  browser: null,
+  installs: null,
+  installCount: null,
+  registered: false,
+
+  unregister: function() {
+    if (!this.registered)
+      return;
+    this.registered = false;
+
+    Services.obs.removeObserver(this, "message-manager-close");
+    // The browser may have already been detached
+    if (this.browser.removeProgressListener)
+      this.browser.removeProgressListener(this);
+
+    for (let install of this.installs)
+      install.removeListener(this);
+    this.installs = null;
+  },
+
+  cancelInstalls: function() {
+    for (let install of this.installs) {
+      try {
+        install.cancel();
+      }
+      catch (e) {
+        // Some installs may have already failed or been cancelled, ignore these
+      }
+    }
+  },
+
+  observe: function(subject, topic, data) {
+    if (subject != this.browser.messageManager)
+      return;
+
+    // The browser's message manager has closed and so the browser is
+    // going away, cancel all installs
+    this.cancelInstalls();
+  },
+
+  onLocationChange: function(webProgress, request, location) {
+    if (this.browser.contentPrincipal && this.principal.subsumes(this.browser.contentPrincipal))
+      return;
+
+    // The browser has navigated to a new origin so cancel all installs
+    this.cancelInstalls();
+  },
+
+  onDownloadCancelled: function(install) {
+    // Don't need to hear more events from this install
+    install.removeListener(this);
+
+    // Once all installs have ended unregister everything
+    if (--this.installCount == 0)
+      this.unregister();
+  },
+
+  onDownloadFailed: function(install) {
+    this.onDownloadCancelled(install);
+  },
+
+  onInstallFailed: function(install) {
+    this.onDownloadCancelled(install);
+  },
+
+  onInstallEnded: function(install) {
+    this.onDownloadCancelled(install);
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference,
+                                         Ci.nsIWebProgressListener,
+                                         Ci.nsIObserver])
+};
+
+/**
  * This represents an author of an add-on (e.g. creator or developer)
  *
  * @param  aName
  *         The name of the author
  * @param  aURL
  *         The URL of the author's profile page
  */
 function AddonAuthor(aName, aURL) {
@@ -1983,73 +2108,73 @@ var AddonManagerInternal = {
   },
 
   /**
    * Checks whether a particular source is allowed to install add-ons of a
    * given mimetype.
    *
    * @param  aMimetype
    *         The mimetype of the add-on
-   * @param  aURI
-   *         The optional nsIURI of the source
+   * @param  aInstallingPrincipal
+   *         The nsIPrincipal that initiated the install
    * @return true if the source is allowed to install this mimetype
    */
-  isInstallAllowed: function AMI_isInstallAllowed(aMimetype, aURI) {
+  isInstallAllowed: function AMI_isInstallAllowed(aMimetype, aInstallingPrincipal) {
     if (!gStarted)
       throw Components.Exception("AddonManager is not initialized",
                                  Cr.NS_ERROR_NOT_INITIALIZED);
 
     if (!aMimetype || typeof aMimetype != "string")
       throw Components.Exception("aMimetype must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
 
-    if (aURI && !(aURI instanceof Ci.nsIURI))
-      throw Components.Exception("aURI must be a nsIURI or null",
+    if (!aInstallingPrincipal || !(aInstallingPrincipal instanceof Ci.nsIPrincipal))
+      throw Components.Exception("aInstallingPrincipal must be a nsIPrincipal",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     let providers = [...this.providers];
     for (let provider of providers) {
       if (callProvider(provider, "supportsMimetype", false, aMimetype) &&
-          callProvider(provider, "isInstallAllowed", null, aURI))
+          callProvider(provider, "isInstallAllowed", null, aInstallingPrincipal))
         return true;
     }
     return false;
   },
 
   /**
    * Starts installation of an array of AddonInstalls notifying the registered
    * web install listener of blocked or started installs.
    *
    * @param  aMimetype
    *         The mimetype of add-ons being installed
    * @param  aBrowser
    *         The optional browser element that started the installs
-   * @param  aURI
-   *         The optional nsIURI that started the installs
+   * @param  aInstallingPrincipal
+   *         The nsIPrincipal that initiated the install
    * @param  aInstalls
    *         The array of AddonInstalls to be installed
    */
   installAddonsFromWebpage: function AMI_installAddonsFromWebpage(aMimetype,
                                                                   aBrowser,
-                                                                  aURI,
+                                                                  aInstallingPrincipal,
                                                                   aInstalls) {
     if (!gStarted)
       throw Components.Exception("AddonManager is not initialized",
                                  Cr.NS_ERROR_NOT_INITIALIZED);
 
     if (!aMimetype || typeof aMimetype != "string")
       throw Components.Exception("aMimetype must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     if (aBrowser && !(aBrowser instanceof Ci.nsIDOMElement))
       throw Components.Exception("aSource must be a nsIDOMElement, or null",
                                  Cr.NS_ERROR_INVALID_ARG);
 
-    if (aURI && !(aURI instanceof Ci.nsIURI))
-      throw Components.Exception("aURI must be a nsIURI or null",
+    if (!aInstallingPrincipal || !(aInstallingPrincipal instanceof Ci.nsIPrincipal))
+      throw Components.Exception("aInstallingPrincipal must be a nsIPrincipal",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     if (!Array.isArray(aInstalls))
       throw Components.Exception("aInstalls must be an array",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     if (!("@mozilla.org/addons/web-install-listener;1" in Cc)) {
       logger.warn("No web installer available, cancelling all installs");
@@ -2058,30 +2183,50 @@ var AddonManagerInternal = {
       });
       return;
     }
 
     try {
       let weblistener = Cc["@mozilla.org/addons/web-install-listener;1"].
                         getService(Ci.amIWebInstallListener);
 
-      if (!this.isInstallEnabled(aMimetype, aURI)) {
-        weblistener.onWebInstallDisabled(aBrowser, aURI, aInstalls,
-                                         aInstalls.length);
+      if (!this.isInstallEnabled(aMimetype)) {
+        for (let install of aInstalls)
+          install.cancel();
+
+        weblistener.onWebInstallDisabled(aBrowser, aInstallingPrincipal.URI,
+                                         aInstalls, aInstalls.length);
+        return;
       }
-      else if (!this.isInstallAllowed(aMimetype, aURI)) {
-        if (weblistener.onWebInstallBlocked(aBrowser, aURI, aInstalls,
-                                            aInstalls.length)) {
+      else if (!aBrowser.contentPrincipal || !aInstallingPrincipal.subsumes(aBrowser.contentPrincipal)) {
+        for (let install of aInstalls)
+          install.cancel();
+
+        if (weblistener instanceof Ci.amIWebInstallListener2) {
+          weblistener.onWebInstallOriginBlocked(aBrowser, aInstallingPrincipal.URI,
+                                                aInstalls, aInstalls.length);
+        }
+        return;
+      }
+
+      // The installs may start now depending on the web install listener,
+      // listen for the browser navigating to a new origin and cancel the
+      // installs in that case.
+      new BrowserListener(aBrowser, aInstallingPrincipal, aInstalls);
+
+      if (!this.isInstallAllowed(aMimetype, aInstallingPrincipal)) {
+        if (weblistener.onWebInstallBlocked(aBrowser, aInstallingPrincipal.URI,
+                                            aInstalls, aInstalls.length)) {
           aInstalls.forEach(function(aInstall) {
             aInstall.install();
           });
         }
       }
-      else if (weblistener.onWebInstallRequested(aBrowser, aURI, aInstalls,
-                                                   aInstalls.length)) {
+      else if (weblistener.onWebInstallRequested(aBrowser, aInstallingPrincipal.URI,
+                                                 aInstalls, aInstalls.length)) {
         aInstalls.forEach(function(aInstall) {
           aInstall.install();
         });
       }
     }
     catch (e) {
       // In the event that the weblistener throws during instantiation or when
       // calling onWebInstallBlocked or onWebInstallRequested all of the
@@ -2926,23 +3071,26 @@ this.AddonManager = {
   mapURIToAddonID: function AM_mapURIToAddonID(aURI) {
     return AddonManagerInternal.mapURIToAddonID(aURI);
   },
 
   isInstallEnabled: function AM_isInstallEnabled(aType) {
     return AddonManagerInternal.isInstallEnabled(aType);
   },
 
-  isInstallAllowed: function AM_isInstallAllowed(aType, aUri) {
-    return AddonManagerInternal.isInstallAllowed(aType, aUri);
+  isInstallAllowed: function AM_isInstallAllowed(aType, aInstallingPrincipal) {
+    return AddonManagerInternal.isInstallAllowed(aType, ensurePrincipal(aInstallingPrincipal));
   },
 
   installAddonsFromWebpage: function AM_installAddonsFromWebpage(aType, aBrowser,
-                                                                 aUri, aInstalls) {
-    AddonManagerInternal.installAddonsFromWebpage(aType, aBrowser, aUri, aInstalls);
+                                                                 aInstallingPrincipal,
+                                                                 aInstalls) {
+    AddonManagerInternal.installAddonsFromWebpage(aType, aBrowser,
+                                                  ensurePrincipal(aInstallingPrincipal),
+                                                  aInstalls);
   },
 
   addManagerListener: function AM_addManagerListener(aListener) {
     AddonManagerInternal.addManagerListener(aListener);
   },
 
   removeManagerListener: function AM_removeManagerListener(aListener) {
     AddonManagerInternal.removeManagerListener(aListener);
--- a/toolkit/mozapps/extensions/addonManager.js
+++ b/toolkit/mozapps/extensions/addonManager.js
@@ -70,32 +70,33 @@ amManager.prototype = {
     return AddonManager.isInstallEnabled(aMimetype);
   },
 
   /**
    * @see amIWebInstaller.idl
    */
   installAddonsFromWebpage: function AMC_installAddonsFromWebpage(aMimetype,
                                                                   aBrowser,
-                                                                  aReferer, aUris,
-                                                                  aHashes, aNames,
-                                                                  aIcons, aCallback) {
+                                                                  aInstallingPrincipal,
+                                                                  aUris, aHashes,
+                                                                  aNames, aIcons,
+                                                                  aCallback) {
     if (aUris.length == 0)
       return false;
 
     let retval = true;
-    if (!AddonManager.isInstallAllowed(aMimetype, aReferer)) {
+    if (!AddonManager.isInstallAllowed(aMimetype, aInstallingPrincipal)) {
       aCallback = null;
       retval = false;
     }
 
     let installs = [];
     function buildNextInstall() {
       if (aUris.length == 0) {
-        AddonManager.installAddonsFromWebpage(aMimetype, aBrowser, aReferer, installs);
+        AddonManager.installAddonsFromWebpage(aMimetype, aBrowser, aInstallingPrincipal, installs);
         return;
       }
       let uri = aUris.shift();
       AddonManager.getInstallForURL(uri, function buildNextInstall_getInstallForURL(aInstall) {
         function callCallback(aUri, aStatus) {
           try {
             aCallback.onInstallEnded(aUri, aStatus);
           }
@@ -147,40 +148,38 @@ amManager.prototype = {
   /**
    * messageManager callback function.
    *
    * Listens to requests from child processes for InstallTrigger
    * activity, and sends back callbacks.
    */
   receiveMessage: function AMC_receiveMessage(aMessage) {
     let payload = aMessage.data;
-    let referer = payload.referer ? Services.io.newURI(payload.referer, null, null)
-                                  : null;
 
     switch (aMessage.name) {
       case MSG_INSTALL_ENABLED:
-        return this.isInstallEnabled(payload.mimetype, referer);
+        return AddonManager.isInstallEnabled(payload.mimetype);
 
       case MSG_INSTALL_ADDONS: {
         let callback = null;
         if (payload.callbackID != -1) {
           callback = {
             onInstallEnded: function ITP_callback(url, status) {
               gParentMM.broadcastAsyncMessage(MSG_INSTALL_CALLBACK, {
                 callbackID: payload.callbackID,
                 url: url,
                 status: status
               });
             },
           };
         }
 
         return this.installAddonsFromWebpage(payload.mimetype,
-          aMessage.target, referer, payload.uris, payload.hashes,
-          payload.names, payload.icons, callback);
+          aMessage.target, payload.triggeringPrincipal, payload.uris,
+          payload.hashes, payload.names, payload.icons, callback);
       }
     }
   },
 
   classID: Components.ID("{4399533d-08d1-458c-a87a-235f74451cfa}"),
   _xpcom_factory: {
     createInstance: function AMC_createInstance(aOuter, aIid) {
       if (aOuter != null)
--- a/toolkit/mozapps/extensions/amContentHandler.js
+++ b/toolkit/mozapps/extensions/amContentHandler.js
@@ -38,39 +38,30 @@ amContentHandler.prototype = {
 
     let window = null;
     let callbacks = aRequest.notificationCallbacks ?
                     aRequest.notificationCallbacks :
                     aRequest.loadGroup.notificationCallbacks;
     if (callbacks)
       window = callbacks.getInterface(Ci.nsIDOMWindow);
 
-    let referer = null;
-    if (aRequest instanceof Ci.nsIPropertyBag2) {
-      referer = aRequest.getPropertyAsInterface("docshell.internalReferrer",
-                                                Ci.nsIURI);
-    }
-
-    if (!referer && aRequest instanceof Ci.nsIHttpChannel)
-      referer = aRequest.referrer;
-
     aRequest.cancel(Cr.NS_BINDING_ABORTED);
 
     let messageManager = window.QueryInterface(Ci.nsIInterfaceRequestor)
                                .getInterface(Ci.nsIDocShell)
                                .QueryInterface(Ci.nsIInterfaceRequestor)
                                .getInterface(Ci.nsIContentFrameMessageManager);
 
     messageManager.sendAsyncMessage(MSG_INSTALL_ADDONS, {
       uris: [uri.spec],
       hashes: [null],
       names: [null],
       icons: [null],
       mimetype: XPI_CONTENT_TYPE,
-      referer: referer ? referer.spec : null,
+      triggeringPrincipal: aRequest.loadInfo.triggeringPrincipal,
       callbackID: -1
     });
   },
 
   classID: Components.ID("{7beb3ba8-6ec3-41b4-b67c-da89b8518922}"),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentHandler]),
 
   log : function XCH_log(aMsg) {
--- a/toolkit/mozapps/extensions/amIWebInstallListener.idl
+++ b/toolkit/mozapps/extensions/amIWebInstallListener.idl
@@ -82,16 +82,37 @@ interface amIWebInstallListener : nsISup
    *         The number of AddonInstalls
    * @return true if the caller should start the installs
    */
   boolean onWebInstallRequested(in nsIDOMElement aBrowser, in nsIURI aUri,
                                 [array, size_is(aCount)] in nsIVariant aInstalls,
                                 [optional] in uint32_t aCount);
 };
 
+[scriptable, uuid(a80b89ad-bb1a-4c43-9cb7-3ae656556f78)]
+interface amIWebInstallListener2 : nsISupports
+{
+  /**
+   * Called when a non-same-origin resource attempted to initiate an install.
+   * Installs will have already been cancelled and cannot be restarted.
+   *
+   * @param  aBrowser
+   *         The browser that triggered the installs
+   * @param  aUri
+   *         The URI of the site that triggered the installs
+   * @param  aInstalls
+   *         The AddonInstalls that were blocked
+   * @param  aCount
+   *         The number of AddonInstalls
+   */
+  boolean onWebInstallOriginBlocked(in nsIDOMElement aBrowser, in nsIURI aUri,
+                                    [array, size_is(aCount)] in nsIVariant aInstalls,
+                                    [optional] in uint32_t aCount);
+};
+
 /**
  * amIWebInstallPrompt is used, if available, by the default implementation of 
  * amIWebInstallInfo to display a confirmation UI to the user before running
  * installs.
  */
 [scriptable, uuid(386906f1-4d18-45bf-bc81-5dcd68e42c3b)]
 interface amIWebInstallPrompt : nsISupports
 {
--- a/toolkit/mozapps/extensions/amInstallTrigger.js
+++ b/toolkit/mozapps/extensions/amInstallTrigger.js
@@ -56,33 +56,32 @@ RemoteMediator.prototype = {
       if (callbackHandler) {
         callbackHandler.callCallback(payload.url, payload.status);
       }
     }
   },
 
   enabled: function(url) {
     let params = {
-      referer: url,
       mimetype: XPINSTALL_MIMETYPE
     };
     return this.mm.sendSyncMessage(MSG_INSTALL_ENABLED, params)[0];
   },
 
-  install: function(installs, referer, callback, window) {
+  install: function(installs, principal, callback, window) {
     let messageManager = window.QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIWebNavigation)
                          .QueryInterface(Ci.nsIDocShell)
                          .QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIContentFrameMessageManager);
 
     let callbackID = this._addCallback(callback, installs.uris);
 
     installs.mimetype = XPINSTALL_MIMETYPE;
-    installs.referer = referer;
+    installs.triggeringPrincipal = principal;
     installs.callbackID = callbackID;
 
     return messageManager.sendSyncMessage(MSG_INSTALL_ADDONS, installs)[0];
   },
 
   _addCallback: function(callback, urls) {
     if (!callback || typeof callback != "function")
       return -1;
@@ -162,17 +161,17 @@ InstallTrigger.prototype = {
       }
 
       installData.uris.push(url.spec);
       installData.hashes.push(item.Hash || null);
       installData.names.push(name);
       installData.icons.push(iconUrl ? iconUrl.spec : null);
     }
 
-    return this._mediator.install(installData, this._url.spec, callback, this._window);
+    return this._mediator.install(installData, this._principal, callback, this._window);
   },
 
   startSoftwareUpdate: function(url, flags) {
     let filename = Services.io.newURI(url, null, null)
                               .QueryInterface(Ci.nsIURL)
                               .filename;
     let args = {};
     args[filename] = { "URL": url };
--- a/toolkit/mozapps/extensions/amWebInstallListener.js
+++ b/toolkit/mozapps/extensions/amWebInstallListener.js
@@ -287,16 +287,35 @@ extWebInstallListener.prototype = {
       QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo])
     };
     Services.obs.notifyObservers(info, "addon-install-disabled", null);
   },
 
   /**
    * @see amIWebInstallListener.idl
    */
+  onWebInstallOriginBlocked: function extWebInstallListener_onWebInstallOriginBlocked(aBrowser, aUri, aInstalls) {
+    let info = {
+      browser: aBrowser,
+      originatingURI: aUri,
+      installs: aInstalls,
+
+      install: function onWebInstallBlocked_install() {
+      },
+
+      QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo])
+    };
+    Services.obs.notifyObservers(info, "addon-install-origin-blocked", null);
+
+    return false;
+  },
+
+  /**
+   * @see amIWebInstallListener.idl
+   */
   onWebInstallBlocked: function extWebInstallListener_onWebInstallBlocked(aBrowser, aUri, aInstalls) {
     let info = {
       browser: aBrowser,
       originatingURI: aUri,
       installs: aInstalls,
 
       install: function onWebInstallBlocked_install() {
         new Installer(this.browser, this.originatingURI, this.installs);
@@ -317,12 +336,13 @@ extWebInstallListener.prototype = {
 
     // We start the installs ourself
     return false;
   },
 
   classDescription: "XPI Install Handler",
   contractID: "@mozilla.org/addons/web-install-listener;1",
   classID: Components.ID("{0f38e086-89a3-40a5-8ffc-9b694de1d04a}"),
-  QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallListener])
+  QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallListener,
+                                         Ci.amIWebInstallListener2])
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([extWebInstallListener]);
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -1245,18 +1245,21 @@ var gViewController = {
 
         var files = fp.files;
         var installs = [];
 
         function buildNextInstall() {
           if (!files.hasMoreElements()) {
             if (installs.length > 0) {
               // Display the normal install confirmation for the installs
-              AddonManager.installAddonsFromWebpage("application/x-xpinstall",
-                                                    getBrowserElement(), null, installs);
+              let webInstaller = Cc["@mozilla.org/addons/web-install-listener;1"].
+                                 getService(Ci.amIWebInstallListener);
+              webInstaller.onWebInstallRequested(getBrowserElement(),
+                                                 document.documentURIObject,
+                                                 installs);
             }
             return;
           }
 
           var file = files.getNext();
           AddonManager.getInstallForFile(file, function cmd_installFromFile_getInstallForFile(aInstall) {
             installs.push(aInstall);
             buildNextInstall();
@@ -3740,18 +3743,21 @@ var gDragDrop = {
 
     var pos = 0;
     var installs = [];
 
     function buildNextInstall() {
       if (pos == urls.length) {
         if (installs.length > 0) {
           // Display the normal install confirmation for the installs
-          AddonManager.installAddonsFromWebpage("application/x-xpinstall",
-                                                getBrowserElement(), null, installs);
+          let webInstaller = Cc["@mozilla.org/addons/web-install-listener;1"].
+                             getService(Ci.amIWebInstallListener);
+          webInstaller.onWebInstallRequested(getBrowserElement(),
+                                             document.documentURIObject,
+                                             installs);
         }
         return;
       }
 
       AddonManager.getInstallForURL(urls[pos++], function onDrop_getInstallForURL(aInstall) {
         installs.push(aInstall);
         buildNextInstall();
       }, "application/x-xpinstall");
--- a/toolkit/mozapps/extensions/content/xpinstallConfirm.js
+++ b/toolkit/mozapps/extensions/content/xpinstallConfirm.js
@@ -3,37 +3,55 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * 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/. */
 
 var XPInstallConfirm = {};
 
 XPInstallConfirm.init = function XPInstallConfirm_init()
 {
+  Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
   var _installCountdown;
   var _installCountdownInterval;
   var _focused;
   var _timeout;
 
   // Default to cancelling the install when the window unloads
   XPInstallConfirm._installOK = false;
 
   var bundle = document.getElementById("xpinstallConfirmStrings");
 
   let args = window.arguments[0].wrappedJSObject;
 
+  // If all installs have already been cancelled in some way then just close
+  // the window
+  if (args.installs.every(i => i.state != AddonManager.STATE_DOWNLOADED)) {
+    window.close();
+    return;
+  }
+
   var _installCountdownLength = 5;
   try {
     var prefs = Components.classes["@mozilla.org/preferences-service;1"]
                           .getService(Components.interfaces.nsIPrefBranch);
     var delay_in_milliseconds = prefs.getIntPref("security.dialog_enable_delay");
     _installCountdownLength = Math.round(delay_in_milliseconds / 500);
   } catch (ex) { }
   
   var itemList = document.getElementById("itemList");
+
+  let installMap = new WeakMap();
+  let installListener = {
+    onDownloadCancelled: function(install) {
+      itemList.removeChild(installMap.get(install));
+      if (--numItemsToInstall == 0)
+        window.close();
+    }
+  };
   
   var numItemsToInstall = args.installs.length;
   for (let install of args.installs) {
     var installItem = document.createElement("installitem");
     itemList.appendChild(installItem);
 
     installItem.name = install.addon.name;
     installItem.url = install.sourceURI.spec;
@@ -45,16 +63,19 @@ XPInstallConfirm.init = function XPInsta
       installItem.type = type;
     if (install.certName) {
       installItem.cert = bundle.getFormattedString("signed", [install.certName]);
     }
     else {
       installItem.cert = bundle.getString("unverified");
     }
     installItem.signed = install.certName ? "true" : "false";
+
+    installMap.set(install, installItem);
+    install.addListener(installListener);
   }
   
   var introString = bundle.getString("itemWarnIntroSingle");
   if (numItemsToInstall > 4)
     introString = bundle.getFormattedString("itemWarnIntroMultiple", [numItemsToInstall]);
   var textNode = document.createTextNode(introString);
   var introNode = document.getElementById("itemWarningIntro");
   while (introNode.hasChildNodes())
@@ -121,25 +142,30 @@ XPInstallConfirm.init = function XPInsta
 
   function myUnload() {
     if (_installCountdownLength > 0) {
       document.removeEventListener("focus", myfocus, true);
       document.removeEventListener("blur", myblur, true);
     }
     window.removeEventListener("unload", myUnload, false);
 
+    for (let install of args.installs)
+      install.removeListener(installListener);
+
     // Now perform the desired action - either install the
     // addons or cancel the installations
     if (XPInstallConfirm._installOK) {
       for (let install of args.installs)
         install.install();
     }
     else {
-      for (let install of args.installs)
-        install.cancel();
+      for (let install of args.installs) {
+        if (install.state != AddonManager.STATE_CANCELLED)
+          install.cancel();
+      }
     }
   }
 
   window.addEventListener("unload", myUnload, false);
 
   if (_installCountdownLength > 0) {
     document.addEventListener("focus", myfocus, true);
     document.addEventListener("blur", myblur, true);
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -4024,46 +4024,48 @@ this.XPIProvider = {
   isFileRequestWhitelisted: function XPI_isFileRequestWhitelisted() {
     // Default to whitelisted if the preference does not exist.
     return Preferences.get(PREF_XPI_FILE_WHITELISTED, true);
   },
 
   /**
    * Called to test whether installing XPI add-ons from a URI is allowed.
    *
-   * @param  aUri
-   *         The URI being installed from
+   * @param  aInstallingPrincipal
+   *         The nsIPrincipal that initiated the install
    * @return true if installing is allowed
    */
-  isInstallAllowed: function XPI_isInstallAllowed(aUri) {
+  isInstallAllowed: function XPI_isInstallAllowed(aInstallingPrincipal) {
     if (!this.isInstallEnabled())
       return false;
 
+    let uri = aInstallingPrincipal.URI;
+
     // Direct requests without a referrer are either whitelisted or blocked.
-    if (!aUri)
+    if (!uri)
       return this.isDirectRequestWhitelisted();
 
     // Local referrers can be whitelisted.
     if (this.isFileRequestWhitelisted() &&
-        (aUri.schemeIs("chrome") || aUri.schemeIs("file")))
+        (uri.schemeIs("chrome") || uri.schemeIs("file")))
       return true;
 
     this.importPermissions();
 
-    let permission = Services.perms.testPermission(aUri, XPI_PERMISSION);
+    let permission = Services.perms.testPermissionFromPrincipal(aInstallingPrincipal, XPI_PERMISSION);
     if (permission == Ci.nsIPermissionManager.DENY_ACTION)
       return false;
 
     let requireWhitelist = Preferences.get(PREF_XPI_WHITELIST_REQUIRED, true);
     if (requireWhitelist && (permission != Ci.nsIPermissionManager.ALLOW_ACTION))
       return false;
 
     let requireSecureOrigin = Preferences.get(PREF_INSTALL_REQUIRESECUREORIGIN, true);
     let safeSchemes = ["https", "chrome", "file"];
-    if (requireSecureOrigin && safeSchemes.indexOf(aUri.scheme) == -1)
+    if (requireSecureOrigin && safeSchemes.indexOf(uri.scheme) == -1)
       return false;
 
     return true;
   },
 
   /**
    * Called to get an AddonInstall to download and install an add-on from a URL.
    *
--- a/toolkit/mozapps/extensions/test/xpcshell/test_blocklistchange.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklistchange.js
@@ -463,56 +463,35 @@ function Pmanual_update(aVersion) {
   for (let name of ["soft1", "soft2", "soft3", "soft4", "soft5", "hard1", "regexp1"]) {
     Pinstalls.push(new Promise((resolve, reject) => {
       AddonManager.getInstallForURL("http://localhost:" + gPort + "/addons/blocklist_"
                                        + name + "_" + aVersion + ".xpi",
                                     resolve, "application/x-xpinstall");
     }));
   }
 
-  return Promise.all(Pinstalls)
-    .then(installs => {
-      return new Promise((resolve, reject) => {
-        Services.obs.addObserver(function(aSubject, aTopic, aData) {
-          Services.obs.removeObserver(arguments.callee, "addon-install-blocked");
-
-          aSubject.QueryInterface(Ci.amIWebInstallInfo);
-
-          var installCount = aSubject.installs.length;
-
-          var listener = {
-            installComplete: function() {
-              installCount--;
-              if (installCount)
-                return;
-
-              resolve();
-            },
+  return Promise.all(Pinstalls).then(installs => {
+    let completePromises = [];
+    for (let install of installs) {
+      completePromises.push(new Promise(resolve => {
+        install.addListener({
+          onDownloadCancelled: resolve,
+          onInstallEnded: resolve
+        })
+      }));
+    }
 
-            onDownloadCancelled: function(aInstall) {
-              this.installComplete();
-            },
-
-            onInstallEnded: function(aInstall) {
-              this.installComplete();
-            }
-          };
+    // Use the default web installer to cancel/allow installs based on whether
+    // the add-on is valid or not.
+    let webInstaller = Cc["@mozilla.org/addons/web-install-listener;1"]
+                       .getService(Ci.amIWebInstallListener);
+    webInstaller.onWebInstallRequested(null, null, installs);
 
-          aSubject.installs.forEach(function(aInstall) {
-            aInstall.addListener(listener);
-          });
-
-          aSubject.install();
-        }, "addon-install-blocked", false);
-
-        AddonManager.installAddonsFromWebpage("application/x-xpinstall", null,
-                                              NetUtil.newURI("http://localhost:" + gPort + "/"),
-                                              installs);
-      })
-    });
+    return Promise.all(completePromises);
+  });
 }
 
 // Checks that an add-ons properties match expected values
 function check_addon(aAddon, aExpectedVersion, aExpectedUserDisabled,
                      aExpectedSoftDisabled, aExpectedState) {
   do_check_neq(aAddon, null);
   do_print("Testing " + aAddon.id + " version " + aAddon.version + " user "
            + aAddon.userDisabled + " soft " + aAddon.softDisabled
--- a/toolkit/mozapps/extensions/test/xpcshell/test_permissions.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_permissions.js
@@ -4,79 +4,83 @@
 
 Components.utils.import("resource://gre/modules/NetUtil.jsm");
 
 // Checks that permissions set in preferences are correctly imported but can
 // be removed by the user.
 
 const XPI_MIMETYPE = "application/x-xpinstall";
 
+function newPrincipal(uri) {
+  return Services.scriptSecurityManager.createCodebasePrincipal(NetUtil.newURI(uri), {});
+}
+
 function run_test() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
 
   Services.prefs.setCharPref("xpinstall.whitelist.add", "https://test1.com,https://test2.com");
   Services.prefs.setCharPref("xpinstall.whitelist.add.36", "https://test3.com,https://www.test4.com");
   Services.prefs.setCharPref("xpinstall.whitelist.add.test5", "https://test5.com");
 
   Services.perms.add(NetUtil.newURI("https://www.test9.com"), "install",
                      AM_Ci.nsIPermissionManager.ALLOW_ACTION);
 
   startupManager();
 
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("http://test1.com")));
+                                               newPrincipal("http://test1.com")));
   do_check_true(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                              NetUtil.newURI("https://test1.com")));
+                                              newPrincipal("https://test1.com")));
   do_check_true(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                              NetUtil.newURI("https://www.test2.com")));
+                                              newPrincipal("https://www.test2.com")));
   do_check_true(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                              NetUtil.newURI("https://test3.com")));
+                                              newPrincipal("https://test3.com")));
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("https://test4.com")));
+                                               newPrincipal("https://test4.com")));
   do_check_true(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                              NetUtil.newURI("https://www.test4.com")));
+                                              newPrincipal("https://www.test4.com")));
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("http://www.test5.com")));
+                                               newPrincipal("http://www.test5.com")));
   do_check_true(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                              NetUtil.newURI("https://www.test5.com")));
+                                              newPrincipal("https://www.test5.com")));
 
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("http://www.test6.com")));
+                                               newPrincipal("http://www.test6.com")));
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("https://www.test6.com")));
+                                               newPrincipal("https://www.test6.com")));
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("https://test7.com")));
+                                               newPrincipal("https://test7.com")));
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("https://www.test8.com")));
+                                               newPrincipal("https://www.test8.com")));
 
   // This should remain unaffected
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("http://www.test9.com")));
+                                               newPrincipal("http://www.test9.com")));
   do_check_true(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                              NetUtil.newURI("https://www.test9.com")));
+                                              newPrincipal("https://www.test9.com")));
 
   Services.perms.removeAll();
 
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("https://test1.com")));
+                                               newPrincipal("https://test1.com")));
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("https://www.test2.com")));
+                                               newPrincipal("https://www.test2.com")));
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("https://test3.com")));
+                                               newPrincipal("https://test3.com")));
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("https://www.test4.com")));
+                                               newPrincipal("https://www.test4.com")));
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("https://www.test5.com")));
+                                               newPrincipal("https://www.test5.com")));
 
   // Upgrade the application and verify that the permissions are still not there
   restartManager("2");
 
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("https://test1.com")));
+                                               newPrincipal("https://test1.com")));
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("https://www.test2.com")));
+                                               newPrincipal("https://www.test2.com")));
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("https://test3.com")));
+                                               newPrincipal("https://test3.com")));
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("https://www.test4.com")));
+                                               newPrincipal("https://www.test4.com")));
   do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
-                                               NetUtil.newURI("https://www.test5.com")));
+                                               newPrincipal("https://www.test5.com")));
 }
--- a/toolkit/mozapps/extensions/test/xpcshell/test_permissions_prefs.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_permissions_prefs.js
@@ -3,16 +3,20 @@
  */
 
 // Tests that xpinstall.[whitelist|blacklist].add preferences are emptied when
 // converted into permissions.
 
 const PREF_XPI_WHITELIST_PERMISSIONS  = "xpinstall.whitelist.add";
 const PREF_XPI_BLACKLIST_PERMISSIONS  = "xpinstall.blacklist.add";
 
+function newPrincipal(uri) {
+  return Services.scriptSecurityManager.createCodebasePrincipal(NetUtil.newURI(uri), {});
+}
+
 function do_check_permission_prefs(preferences) {
   // Check preferences were emptied
   for (let pref of preferences) {
     try {
       do_check_eq(Services.prefs.getCharPref(pref), "");
     }
     catch (e) {
       // Successfully emptied
@@ -38,18 +42,17 @@ function run_test() {
   var whitelistPreferences = Services.prefs.getChildList(PREF_XPI_WHITELIST_PERMISSIONS, {});
   var blacklistPreferences = Services.prefs.getChildList(PREF_XPI_BLACKLIST_PERMISSIONS, {});
   var preferences = whitelistPreferences.concat(blacklistPreferences);
 
   startupManager();
 
   // Permissions are imported lazily - act as thought we're checking an install,
   // to trigger on-deman importing of the permissions.
-  let url = Services.io.newURI("http://example.com/file.xpi", null, null);
-  AddonManager.isInstallAllowed("application/x-xpinstall", url);
+  AddonManager.isInstallAllowed("application/x-xpinstall", newPrincipal("http://example.com/file.xpi"));
   do_check_permission_prefs(preferences);
 
 
   // Import can also be triggerred by an observer notification by any other area
   // of code, such as a permissions management UI.
 
   // First, request to flush all permissions
   clear_imported_preferences_cache();
--- a/toolkit/mozapps/extensions/test/xpinstall/browser.ini
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser.ini
@@ -11,16 +11,17 @@ support-files =
   enabled.html
   hashRedirect.sjs
   head.js
   incompatible.xpi
   installchrome.html
   installtrigger.html
   installtrigger_frame.html
   multipackage.xpi
+  navigate.html
   redirect.sjs
   restartless.xpi
   signed-no-cn.xpi
   signed-no-o.xpi
   signed-tampered.xpi
   signed-untrusted.xpi
   signed.xpi
   signed2.xpi
@@ -48,16 +49,17 @@ skip-if = true # disabled due to a leak.
 [browser_cancel.js]
 [browser_concurrent_installs.js]
 [browser_cookies.js]
 [browser_cookies2.js]
 [browser_cookies3.js]
 [browser_cookies4.js]
 skip-if = true # Bug 1084646
 [browser_corrupt.js]
+[browser_datauri.js]
 [browser_empty.js]
 [browser_enabled.js]
 [browser_enabled2.js]
 [browser_enabled3.js]
 [browser_hash.js]
 [browser_httphash.js]
 [browser_httphash2.js]
 [browser_httphash3.js]
@@ -67,30 +69,34 @@ skip-if = true # Bug 1084646
 [browser_installchrome.js]
 [browser_localfile.js]
 [browser_localfile2.js]
 [browser_localfile3.js]
 [browser_localfile4.js]
 [browser_multipackage.js]
 [browser_navigateaway.js]
 [browser_navigateaway2.js]
+[browser_navigateaway3.js]
+[browser_navigateaway4.js]
 [browser_offline.js]
 [browser_relative.js]
 [browser_signed_multiple.js]
 [browser_signed_naming.js]
 [browser_signed_tampered.js]
 [browser_signed_trigger.js]
 [browser_signed_untrusted.js]
 [browser_signed_url.js]
 [browser_softwareupdate.js]
 [browser_switchtab.js]
 [browser_trigger_redirect.js]
 [browser_unsigned_trigger.js]
 [browser_unsigned_trigger_iframe.js]
 skip-if = buildapp == "mulet"
+[browser_unsigned_trigger_xorigin.js]
+skip-if = buildapp == "mulet"
 [browser_unsigned_url.js]
 [browser_whitelist.js]
 [browser_whitelist2.js]
 [browser_whitelist3.js]
 [browser_whitelist4.js]
 [browser_whitelist5.js]
 [browser_whitelist6.js]
 [browser_whitelist7.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_datauri.js
@@ -0,0 +1,37 @@
+// ----------------------------------------------------------------------------
+// Checks that a chained redirect through a data URI and javascript is blocked
+
+function setup_redirect(aSettings) {
+  var url = TESTROOT + "redirect.sjs?mode=setup";
+  for (var name in aSettings) {
+    url += "&" + name + "=" + encodeURIComponent(aSettings[name]);
+  }
+
+  var req = new XMLHttpRequest();
+  req.open("GET", url, false);
+  req.send(null);
+}
+
+function test() {
+  Harness.installOriginBlockedCallback = install_blocked;
+  Harness.installsCompletedCallback = finish_test;
+  Harness.setup();
+
+  setup_redirect({
+    "Location": "data:text/html,<script>window.location.href='" + TESTROOT + "unsigned.xpi'</script>"
+  });
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.loadURI(TESTROOT + "redirect.sjs?mode=redirect");
+}
+
+function install_blocked(installInfo) {
+}
+
+function finish_test(count) {
+  is(count, 0, "No add-ons should have been installed");
+  Services.perms.remove(makeURI("http://example.com"), "install");
+
+  gBrowser.removeCurrentTab();
+  Harness.finish();
+}
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash6.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash6.js
@@ -60,17 +60,17 @@ function finish_failed_download() {
   });
 
   // The harness expects onNewInstall events for all installs that are about to start
   Harness.onNewInstall(gInstall);
 
   // Restart the install as a regular webpage install so the harness tracks it
   AddonManager.installAddonsFromWebpage("application/x-xpinstall",
                                         gBrowser.selectedBrowser,
-                                        gBrowser.currentURI, [gInstall]);
+                                        gBrowser.contentPrincipal, [gInstall]);
 }
 
 function install_ended(install, addon) {
   install.cancel();
 }
 
 function finish_test(count) {
   is(count, 1, "1 Add-on should have been successfully installed");
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile.js
@@ -9,18 +9,21 @@ function test() {
                      .getService(Components.interfaces.nsIChromeRegistry);
 
   var chromeroot = extractChromeRoot(gTestPath);
   try {
     var xpipath = cr.convertChromeURL(makeURI(chromeroot + "unsigned.xpi")).spec;
   } catch (ex) {
     var xpipath = chromeroot + "unsigned.xpi"; //scenario where we are running from a .jar and already extracted
   }
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.loadURI(xpipath);
+
+  gBrowser.selectedTab = gBrowser.addTab("about:blank");
+  BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+    gBrowser.loadURI(xpipath);
+  });
 }
 
 function install_ended(install, addon) {
   install.cancel();
 }
 
 function finish_test(count) {
   is(count, 1, "1 Add-on should have been successfully installed");
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js
@@ -13,18 +13,21 @@ function test() {
                      .getService(Components.interfaces.nsIChromeRegistry);
 
   var chromeroot = extractChromeRoot(gTestPath);
   try {
     var xpipath = cr.convertChromeURL(makeURI(chromeroot + "unsigned.xpi")).spec;
   } catch (ex) {
     var xpipath = chromeroot + "unsigned.xpi"; //scenario where we are running from a .jar and already extracted
   }
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.loadURI(xpipath);
+
+  gBrowser.selectedTab = gBrowser.addTab("about:blank");
+  BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+    gBrowser.loadURI(xpipath);
+  });
 }
 
 function allow_blocked(installInfo) {
   ok(true, "Seen blocked");
   return false;
 }
 
 function finish_test(count) {
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_multipackage.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_multipackage.js
@@ -1,18 +1,20 @@
 // ----------------------------------------------------------------------------
 // Tests installing an signed add-on by navigating directly to the url
 function test() {
   Harness.installConfirmCallback = confirm_install;
   Harness.installEndedCallback = install_ended;
   Harness.installsCompletedCallback = finish_test;
   Harness.setup();
 
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.loadURI(TESTROOT + "multipackage.xpi");
+  gBrowser.selectedTab = gBrowser.addTab("about:blank");
+  BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+    gBrowser.loadURI(TESTROOT + "multipackage.xpi");
+  });
 }
 
 function get_item(items, name) {
   for (let item of items) {
     if (item.name == name)
       return item;
   }
   ok(false, "Item for " + name + " was not listed");
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway.js
@@ -14,17 +14,17 @@ function test() {
   var triggers = encodeURIComponent(JSON.stringify({
     "Unsigned XPI": TESTROOT + "unsigned.xpi"
   }));
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
 }
 
 function download_progress(addon, value, maxValue) {
-  gBrowser.loadURI("about:blank");
+  gBrowser.loadURI(TESTROOT + "enabled.html");
 }
 
 function install_ended(install, addon) {
   install.cancel();
 }
 
 function finish_test(count) {
   is(count, 1, "1 Add-on should have been successfully installed");
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway2.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway2.js
@@ -1,12 +1,11 @@
 // ----------------------------------------------------------------------------
-// Tests that closing the initiating page during the install doesn't break the
-// install.
-// This verifies bugs 473060 and 475347
+// Tests that closing the initiating page during the install cancels the install
+// to avoid spoofing the user.
 function test() {
   Harness.downloadProgressCallback = download_progress;
   Harness.installEndedCallback = install_ended;
   Harness.installsCompletedCallback = finish_test;
   Harness.setup();
 
   var pm = Services.perms;
   pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
@@ -18,18 +17,18 @@ function test() {
   gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
 }
 
 function download_progress(addon, value, maxValue) {
   gBrowser.removeCurrentTab();
 }
 
 function install_ended(install, addon) {
-  install.cancel();
+  ok(false, "Should not have seen installs complete");
 }
 
 function finish_test(count) {
-  is(count, 1, "1 Add-on should have been successfully installed");
+  is(count, 0, "No add-ons should have been successfully installed");
 
   Services.perms.remove(makeURI("http://example.com"), "install");
 
   Harness.finish();
 }
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway3.js
@@ -0,0 +1,38 @@
+// ----------------------------------------------------------------------------
+// Tests that navigating to a new origin cancels ongoing installs.
+
+// Block the modal install UI from showing.
+Services.prefs.setBoolPref(PREF_CUSTOM_CONFIRMATION_UI, true);
+
+function test() {
+  Harness.downloadProgressCallback = download_progress;
+  Harness.installEndedCallback = install_ended;
+  Harness.installsCompletedCallback = finish_test;
+  Harness.setup();
+
+  var pm = Services.perms;
+  pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+  var triggers = encodeURIComponent(JSON.stringify({
+    "Unsigned XPI": TESTROOT + "unsigned.xpi"
+  }));
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function download_progress(addon, value, maxValue) {
+  gBrowser.loadURI(TESTROOT2 + "enabled.html");
+}
+
+function install_ended(install, addon) {
+  ok(false, "Should not have seen installs complete");
+}
+
+function finish_test(count) {
+  is(count, 0, "No add-ons should have been successfully installed");
+
+  Services.perms.remove(makeURI("http://example.com"), "install");
+
+  gBrowser.removeCurrentTab();
+  Harness.finish();
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway4.js
@@ -0,0 +1,44 @@
+// ----------------------------------------------------------------------------
+// Tests that navigating to a new origin cancels ongoing installs and closes
+// the install UI.
+let sawUnload = null;
+
+function test() {
+  Harness.installConfirmCallback = confirm_install;
+  Harness.installEndedCallback = install_ended;
+  Harness.installsCompletedCallback = finish_test;
+  Harness.setup();
+
+  var pm = Services.perms;
+  pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+  var triggers = encodeURIComponent(JSON.stringify({
+    "Unsigned XPI": TESTROOT + "unsigned.xpi"
+  }));
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function confirm_install(window) {
+  sawUnload = BrowserTestUtils.waitForEvent(window, "unload");
+
+  gBrowser.loadURI(TESTROOT2 + "enabled.html");
+
+  return Harness.leaveOpen;
+}
+
+function install_ended(install, addon) {
+  ok(false, "Should not have seen installs complete");
+}
+
+function finish_test(count) {
+  is(count, 0, "No add-ons should have been successfully installed");
+
+  Services.perms.remove(makeURI("http://example.com"), "install");
+
+  sawUnload.then(() => {
+    ok(true, "The install UI should have closed itself.");
+    gBrowser.removeCurrentTab();
+    Harness.finish();
+  });
+}
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_signed_url.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_signed_url.js
@@ -1,18 +1,20 @@
 // ----------------------------------------------------------------------------
 // Tests installing an signed add-on by navigating directly to the url
 function test() {
   Harness.installConfirmCallback = confirm_install;
   Harness.installEndedCallback = install_ended;
   Harness.installsCompletedCallback = finish_test;
   Harness.setup();
 
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.loadURI(TESTROOT + "signed.xpi");
+  gBrowser.selectedTab = gBrowser.addTab("about:blank");
+  BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+    gBrowser.loadURI(TESTROOT + "signed.xpi");
+  });
 }
 
 function confirm_install(window) {
   let items = window.document.getElementById("itemList").childNodes;
   is(items.length, 1, "Should only be 1 item listed in the confirmation dialog");
   is(items[0].name, "Signed XPI Test", "Should have had the name");
   is(items[0].url, TESTROOT + "signed.xpi", "Should have listed the correct url for the item");
   is(items[0].cert, "(Object Signer)", "Should have seen the signer");
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_iframe.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_iframe.js
@@ -7,25 +7,25 @@ function test() {
   Harness.installEndedCallback = install_ended;
   Harness.installsCompletedCallback = finish_test;
   Harness.finalContentEvent = "InstallComplete";
   Harness.setup();
 
   var pm = Services.perms;
   pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
 
-  var triggers = encodeURIComponent(JSON.stringify({
+  var inner_url = encodeURIComponent(TESTROOT + "installtrigger.html?" + encodeURIComponent(JSON.stringify({
     "Unsigned XPI": {
       URL: TESTROOT + "unsigned.xpi",
       IconURL: TESTROOT + "icon.png",
       toString: function() { return this.URL; }
     }
-  }));
+  })));
   gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.loadURI(TESTROOT + "installtrigger_frame.html?" + triggers);
+  gBrowser.loadURI(TESTROOT + "installtrigger_frame.html?" + inner_url);
 }
 
 function confirm_install(window) {
   var items = window.document.getElementById("itemList").childNodes;
   is(items.length, 1, "Should only be 1 item listed in the confirmation dialog");
   is(items[0].name, "XPI Test", "Should have seen the name");
   is(items[0].url, TESTROOT + "unsigned.xpi", "Should have listed the correct url for the item");
   is(items[0].icon, TESTROOT + "icon.png", "Should have listed the correct icon for the item");
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_xorigin.js
@@ -0,0 +1,38 @@
+// ----------------------------------------------------------------------------
+// Ensure that an inner frame from a different origin can't initiate an install
+
+let wasOriginBlocked = false;
+
+function test() {
+  Harness.installOriginBlockedCallback = install_blocked;
+  Harness.installsCompletedCallback = finish_test;
+  Harness.finalContentEvent = "InstallComplete";
+  Harness.setup();
+
+  var pm = Services.perms;
+  pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+  var inner_url = encodeURIComponent(TESTROOT + "installtrigger.html?" + encodeURIComponent(JSON.stringify({
+    "Unsigned XPI": {
+      URL: TESTROOT + "unsigned.xpi",
+      IconURL: TESTROOT + "icon.png",
+      toString: function() { return this.URL; }
+    }
+  })));
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.loadURI(TESTROOT2 + "installtrigger_frame.html?" + inner_url);
+}
+
+function install_blocked(installInfo) {
+  wasOriginBlocked = true;
+}
+
+function finish_test(count) {
+  ok(wasOriginBlocked, "Should have been blocked due to the cross origin request.");
+
+  is(count, 0, "No add-ons should have been installed");
+  Services.perms.remove(makeURI("http://example.com"), "install");
+
+  gBrowser.removeCurrentTab();
+  Harness.finish();
+}
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_url.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_url.js
@@ -1,18 +1,20 @@
 // ----------------------------------------------------------------------------
 // Tests installing an unsigned add-on by navigating directly to the url
 function test() {
   Harness.installConfirmCallback = confirm_install;
   Harness.installEndedCallback = install_ended;
   Harness.installsCompletedCallback = finish_test;
   Harness.setup();
 
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.loadURI(TESTROOT + "unsigned.xpi");
+  gBrowser.selectedTab = gBrowser.addTab("about:blank");
+  BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+    gBrowser.loadURI(TESTROOT + "unsigned.xpi");
+  });
 }
 
 function confirm_install(window) {
   let items = window.document.getElementById("itemList").childNodes;
   is(items.length, 1, "Should only be 1 item listed in the confirmation dialog");
   is(items[0].name, "XPI Test", "Should have had the filename for the item name");
   is(items[0].url, TESTROOT + "unsigned.xpi", "Should have listed the correct url for the item");
   is(items[0].icon, "", "Should have listed no icon for the item");
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist3.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist3.js
@@ -1,24 +1,23 @@
 // ----------------------------------------------------------------------------
 // Tests installing an unsigned add-on through a navigation. Should not be
 // blocked since the referer is whitelisted.
+let URL = TESTROOT2 + "navigate.html?" + encodeURIComponent(TESTROOT + "unsigned.xpi");
+
 function test() {
   Harness.installConfirmCallback = confirm_install;
   Harness.installsCompletedCallback = finish_test;
   Harness.setup();
 
   var pm = Services.perms;
   pm.add(makeURI("http://example.org/"), "install", pm.ALLOW_ACTION);
 
-  var triggers = encodeURIComponent(JSON.stringify({
-    "Unsigned XPI": TESTROOT2 + "unsigned.xpi"
-  }));
   gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.loadURI(TESTROOT + "unsigned.xpi", makeURI(TESTROOT2 + "test.html"));
+  gBrowser.loadURI(URL);
 }
 
 function confirm_install(window) {
   return false;
 }
 
 function finish_test(count) {
   is(count, 0, "No add-ons should have been installed");
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist4.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist4.js
@@ -1,29 +1,28 @@
 // ----------------------------------------------------------------------------
 // Tests installing an unsigned add-on through a navigation. Should be
 // blocked since the referer is not whitelisted even though the target is.
+let URL = TESTROOT2 + "navigate.html?" + encodeURIComponent(TESTROOT + "unsigned.xpi");
+
 function test() {
   Harness.installBlockedCallback = allow_blocked;
   Harness.installsCompletedCallback = finish_test;
   Harness.setup();
 
   var pm = Services.perms;
   pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
 
-  var triggers = encodeURIComponent(JSON.stringify({
-    "Unsigned XPI": TESTROOT2 + "unsigned.xpi"
-  }));
   gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.loadURI(TESTROOT + "unsigned.xpi", makeURI(TESTROOT2 + "test.html"));
+  gBrowser.loadURI(URL);
 }
 
 function allow_blocked(installInfo) {
   is(installInfo.browser, gBrowser.selectedBrowser, "Install should have been triggered by the right browser");
-  is(installInfo.originatingURI.spec, TESTROOT2 + "test.html", "Install should have been triggered by the right uri");
+  is(installInfo.originatingURI.spec, URL, "Install should have been triggered by the right uri");
   return false;
 }
 
 function finish_test(count) {
   is(count, 0, "No add-ons should have been installed");
   Services.perms.remove(makeURI("http://example.com"), "install");
 
   gBrowser.removeCurrentTab();
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist7.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist7.js
@@ -5,18 +5,20 @@
 function test() {
   Harness.installBlockedCallback = allow_blocked;
   Harness.installsCompletedCallback = finish_test;
   Harness.setup();
 
   // Disable direct request whitelisting, installing should be blocked.
   Services.prefs.setBoolPref("xpinstall.whitelist.directRequest", false);
 
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.loadURI(TESTROOT + "unsigned.xpi");
+  gBrowser.selectedTab = gBrowser.addTab("about:blank");
+  BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+    gBrowser.loadURI(TESTROOT + "unsigned.xpi");
+  });
 }
 
 function allow_blocked(installInfo) {
   ok(true, "Seen blocked");
   return false;
 }
 
 function finish_test(count) {
--- a/toolkit/mozapps/extensions/test/xpinstall/head.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/head.js
@@ -41,16 +41,18 @@ registerCleanupFunction(() => {
  */
 var Harness = {
   // If set then the callback is called when an install is attempted and
   // software installation is disabled.
   installDisabledCallback: null,
   // If set then the callback is called when an install is attempted and
   // then canceled.
   installCancelledCallback: null,
+  // If set then the callback will be called when an install's origin is blocked.
+  installOriginBlockedCallback: null,
   // If set then the callback will be called when an install is blocked by the
   // whitelist. The callback should return true to continue with the install
   // anyway.
   installBlockedCallback: null,
   // If set will be called in the event of authentication being needed to get
   // the xpi. Should return a 2 element array of username and password, or
   // null to not authenticate.
   authenticationCallback: null,
@@ -84,41 +86,47 @@ var Harness = {
 
   waitingForEvent: false,
   pendingCount: null,
   installCount: null,
   runningInstalls: null,
 
   waitingForFinish: false,
 
+  // A unique value to return from the installConfirmCallback to indicate that
+  // the install UI shouldn't be closed automatically
+  leaveOpen: {},
+
   // Setup and tear down functions
   setup: function() {
     if (!this.waitingForFinish) {
       waitForExplicitFinish();
       this.waitingForFinish = true;
 
       Services.prefs.setBoolPref(PREF_INSTALL_REQUIRESECUREORIGIN, false);
 
       Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true);
       Services.obs.addObserver(this, "addon-install-started", false);
       Services.obs.addObserver(this, "addon-install-disabled", false);
+      Services.obs.addObserver(this, "addon-install-origin-blocked", false);
       Services.obs.addObserver(this, "addon-install-blocked", false);
       Services.obs.addObserver(this, "addon-install-failed", false);
       Services.obs.addObserver(this, "addon-install-complete", false);
 
       AddonManager.addInstallListener(this);
 
       Services.wm.addListener(this);
 
       var self = this;
       registerCleanupFunction(function() {
         Services.prefs.clearUserPref(PREF_LOGGING_ENABLED);
         Services.prefs.clearUserPref(PREF_INSTALL_REQUIRESECUREORIGIN);
         Services.obs.removeObserver(self, "addon-install-started");
         Services.obs.removeObserver(self, "addon-install-disabled");
+        Services.obs.removeObserver(self, "addon-install-origin-blocked");
         Services.obs.removeObserver(self, "addon-install-blocked");
         Services.obs.removeObserver(self, "addon-install-failed");
         Services.obs.removeObserver(self, "addon-install-complete");
 
         AddonManager.removeInstallListener(self);
 
         Services.wm.removeListener(self);
 
@@ -145,16 +153,17 @@ var Harness = {
     let callback = this.installsCompletedCallback;
     let count = this.installCount;
 
     is(this.runningInstalls.length, 0, "Should be no running installs left");
     this.runningInstalls.forEach(function(aInstall) {
       info("Install for " + aInstall.sourceURI + " is in state " + aInstall.state);
     });
 
+    this.installOriginBlockedCallback = null;
     this.installBlockedCallback = null;
     this.authenticationCallback = null;
     this.installConfirmCallback = null;
     this.downloadStartedCallback = null;
     this.downloadProgressCallback = null;
     this.downloadCancelledCallback = null;
     this.downloadFailedCallback = null;
     this.downloadEndedCallback = null;
@@ -172,17 +181,24 @@ var Harness = {
   windowReady: function(window) {
     if (window.document.location.href == XPINSTALL_URL) {
       if (this.installBlockedCallback)
         ok(false, "Should have been blocked by the whitelist");
       this.pendingCount = window.document.getElementById("itemList").childNodes.length;
 
       // If there is a confirm callback then its return status determines whether
       // to install the items or not. If not the test is over.
-      if (this.installConfirmCallback && !this.installConfirmCallback(window)) {
+      let result = true;
+      if (this.installConfirmCallback) {
+        result = this.installConfirmCallback(window);
+        if (result === this.leaveOpen)
+          return;
+      }
+
+      if (!result) {
         window.document.documentElement.cancelDialog();
       }
       else {
         // Initially the accept button is disabled on a countdown timer
         var button = window.document.documentElement.getButton("accept");
         button.disabled = false;
         window.document.documentElement.acceptDialog();
       }
@@ -241,16 +257,23 @@ var Harness = {
       return;
 
     ok(!!this.installCancelledCallback, "Installation shouldn't have been cancelled");
     if (this.installCancelledCallback)
       this.installCancelledCallback(installInfo);
     this.endTest();
   },
 
+  installOriginBlocked: function(installInfo) {
+    ok(!!this.installOriginBlockedCallback, "Shouldn't have been blocked");
+    if (this.installOriginBlockedCallback)
+      this.installOriginBlockedCallback(installInfo);
+    this.endTest();
+  },
+
   installBlocked: function(installInfo) {
     ok(!!this.installBlockedCallback, "Shouldn't have been blocked by the whitelist");
     if (this.installBlockedCallback && this.installBlockedCallback(installInfo)) {
       this.installBlockedCallback = null;
       installInfo.install();
     }
     else {
       this.expectingCancelled = true;
@@ -366,16 +389,19 @@ var Harness = {
          "Should have seen the expected number of installs started");
       break;
     case "addon-install-disabled":
       this.installDisabled(installInfo);
       break;
     case "addon-install-cancelled":
       this.installCancelled(installInfo);
       break;
+    case "addon-install-origin-blocked":
+      this.installOriginBlocked(installInfo);
+      break;
     case "addon-install-blocked":
       this.installBlocked(installInfo);
       break;
     case "addon-install-failed":
       installInfo.installs.forEach(function(aInstall) {
         isnot(this.runningInstalls.indexOf(aInstall), -1,
               "Should only see failures for started installs");
 
--- a/toolkit/mozapps/extensions/test/xpinstall/installtrigger_frame.html
+++ b/toolkit/mozapps/extensions/test/xpinstall/installtrigger_frame.html
@@ -1,24 +1,24 @@
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
           "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
 <html>
 
-<!-- This page will accept some json as the uri query and pass it to
+<!-- This page will accept some url as the uri query and load it in
      an inner iframe, which will run InstallTrigger.install -->
 
 <head>
 <title>InstallTrigger frame tests</title>
 <script type="text/javascript">
 function prepChild() {
   // Pass our parameters over to the child
   var child = window.frames[0];
-  var params = document.location.search.substr(1);
-  child.location = "installtrigger.html?" + params;
+  var url = decodeURIComponent(document.location.search.substr(1));
+  child.location = url;
 }
 </script>
 </head>
 <body onload="prepChild()">
 
 <iframe src="about:blank">
 </iframe>
 
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/navigate.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html>
+
+<!-- This page will accept some url as the uri query and navigate to it by
+     clicking a link -->
+
+<head>
+<title>Navigation tests</title>
+<script type="text/javascript">
+function navigate() {
+  // Pass our parameters over to the child
+  var child = window.frames[0];
+  var url = decodeURIComponent(document.location.search.substr(1));
+  var link = document.getElementById("link");
+  link.href = url;
+  link.click();
+}
+</script>
+</head>
+<body onload="navigate()">
+
+<p><a id="link">Test Link</a></p>
+</body>
+</html>
--- a/toolkit/mozapps/extensions/test/xpinstall/redirect.sjs
+++ b/toolkit/mozapps/extensions/test/xpinstall/redirect.sjs
@@ -4,17 +4,17 @@
 
 function handleRequest(request, response)
 {
   let parts = request.queryString.split("&");
   let settings = {};
 
   parts.forEach(function(aString) {
     let [k, v] = aString.split("=");
-    settings[k] = v;
+    settings[k] = decodeURIComponent(v);
   })
 
   if (settings.mode == "setup") {
     delete settings.mode;
 
     // Object states must be an nsISupports
     var state = {
       settings: settings,