Bug 1042699 - Block cross-origin add-on install requests. r+a=dveditz
authorDave Townsend <dtownsend@oxymoronical.com>
Tue, 18 Aug 2015 17:21:05 -0700
changeset 288901 05f2f11077f58a0d417733cd55f6d95ae884c7dd
parent 288900 4386fc15cc9d1ab1a06f924ddfcb756759668a34
child 288902 66e9b94c406649129e114f4b230e4aa0d9f34611
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1042699
milestone42.0a2
Bug 1042699 - Block cross-origin add-on install requests. r+a=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
@@ -1250,16 +1250,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();
@@ -1567,16 +1568,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
@@ -4019,46 +4019,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,