Bug 1409202 - Web Authentication - Restrict to selected tabs in the active window r=jcj
authorTim Taubert <ttaubert@mozilla.com>
Sat, 10 Mar 2018 06:43:20 +0100
changeset 407439 dd8202671cf14a9cb5e73b918edad64f386e9fb5
parent 407438 792ab44dd9ec02732ae1d964c1726967e05b598f
child 407440 78e2a853d8f8837db876b58a9cd2b6b87bda1de0
child 407500 0817a733d45a48800e68c9e2a5035fd17bfcdee2
push id100693
push userttaubert@mozilla.com
push dateSat, 10 Mar 2018 05:52:36 +0000
treeherdermozilla-inbound@dd8202671cf1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjcj
bugs1409202
milestone60.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1409202 - Web Authentication - Restrict to selected tabs in the active window r=jcj Summary: This patch restricts any calls to navigator.credentials.* methods to selected tabs. Any active WebAuthn request will be aborted when the parent chrome window loses focus, or the <browser> is backgrounded. Reviewers: jcj Reviewed By: jcj Bug #: 1409202 Differential Revision: https://phabricator.services.mozilla.com/D688
dom/credentialmanagement/CredentialsContainer.cpp
dom/credentialmanagement/moz.build
dom/credentialmanagement/tests/browser/.eslintrc.js
dom/credentialmanagement/tests/browser/browser.ini
dom/credentialmanagement/tests/browser/browser_active_document.js
dom/webauthn/WebAuthnManagerBase.cpp
dom/webauthn/tests/browser/browser_abort_visibility.js
--- a/dom/credentialmanagement/CredentialsContainer.cpp
+++ b/dom/credentialmanagement/CredentialsContainer.cpp
@@ -3,16 +3,18 @@
 /* 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/. */
 
 #include "mozilla/dom/CredentialsContainer.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/WebAuthnManager.h"
 #include "nsContentUtils.h"
+#include "nsFocusManager.h"
+#include "nsIDocShell.h"
 
 namespace mozilla {
 namespace dom {
 
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CredentialsContainer, mParent, mManager)
 NS_IMPL_CYCLE_COLLECTING_ADDREF(CredentialsContainer)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(CredentialsContainer)
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CredentialsContainer)
@@ -35,17 +37,60 @@ CreateAndReject(nsPIDOMWindowInner* aPar
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
   promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
   return promise.forget();
 }
 
-bool
+static bool
+IsInActiveTab(nsPIDOMWindowInner* aParent)
+{
+  // Returns whether aParent is an inner window somewhere in the active tab.
+  // The active tab is the selected (i.e. visible) tab in the focused window.
+  MOZ_ASSERT(aParent);
+
+  nsCOMPtr<nsIDocument> doc(aParent->GetExtantDoc());
+  if (NS_WARN_IF(!doc)) {
+    return false;
+  }
+
+  nsCOMPtr<nsIDocShell> docShell = doc->GetDocShell();
+  if (!docShell) {
+    return false;
+  }
+
+  bool isActive = false;
+  docShell->GetIsActive(&isActive);
+  if (!isActive) {
+    return false;
+  }
+
+  nsCOMPtr<nsIDocShellTreeItem> rootItem;
+  docShell->GetRootTreeItem(getter_AddRefs(rootItem));
+  if (!rootItem) {
+    return false;
+  }
+  nsCOMPtr<nsPIDOMWindowOuter> rootWin = rootItem->GetWindow();
+  if (!rootWin) {
+    return false;
+  }
+
+  nsIFocusManager* fm = nsFocusManager::GetFocusManager();
+  if (!fm) {
+    return false;
+  }
+
+  nsCOMPtr<mozIDOMWindowProxy> activeWindow;
+  fm->GetActiveWindow(getter_AddRefs(activeWindow));
+  return activeWindow == rootWin;
+}
+
+static bool
 IsSameOriginWithAncestors(nsPIDOMWindowInner* aParent)
 {
   // This method returns true if aParent is either not in a frame / iframe, or
   // is in a frame or iframe and all ancestors for aParent are the same origin.
   // This is useful for Credential Management because we need to prohibit
   // iframes, but not break mochitests (which use iframes to embed the tests).
   MOZ_ASSERT(aParent);
 
@@ -101,49 +146,43 @@ CredentialsContainer::WrapObject(JSConte
 {
   return CredentialsContainerBinding::Wrap(aCx, this, aGivenProto);
 }
 
 already_AddRefed<Promise>
 CredentialsContainer::Get(const CredentialRequestOptions& aOptions,
                           ErrorResult& aRv)
 {
-  if (!IsSameOriginWithAncestors(mParent)) {
+  if (!IsSameOriginWithAncestors(mParent) || !IsInActiveTab(mParent)) {
     return CreateAndReject(mParent, aRv);
   }
 
-  // TODO: Check that we're an active document, too. See bug 1409202.
-
   EnsureWebAuthnManager();
   return mManager->GetAssertion(aOptions.mPublicKey, aOptions.mSignal);
 }
 
 already_AddRefed<Promise>
 CredentialsContainer::Create(const CredentialCreationOptions& aOptions,
                              ErrorResult& aRv)
 {
-  if (!IsSameOriginWithAncestors(mParent)) {
+  if (!IsSameOriginWithAncestors(mParent) || !IsInActiveTab(mParent)) {
     return CreateAndReject(mParent, aRv);
   }
 
-  // TODO: Check that we're an active document, too. See bug 1409202.
-
   EnsureWebAuthnManager();
   return mManager->MakeCredential(aOptions.mPublicKey, aOptions.mSignal);
 }
 
 already_AddRefed<Promise>
 CredentialsContainer::Store(const Credential& aCredential, ErrorResult& aRv)
 {
-  if (!IsSameOriginWithAncestors(mParent)) {
+  if (!IsSameOriginWithAncestors(mParent) || !IsInActiveTab(mParent)) {
     return CreateAndReject(mParent, aRv);
   }
 
-  // TODO: Check that we're an active document, too. See bug 1409202.
-
   EnsureWebAuthnManager();
   return mManager->Store(aCredential);
 }
 
 already_AddRefed<Promise>
 CredentialsContainer::PreventSilentAccess(ErrorResult& aRv)
 {
   nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
--- a/dom/credentialmanagement/moz.build
+++ b/dom/credentialmanagement/moz.build
@@ -17,8 +17,9 @@ UNIFIED_SOURCES += [
     'CredentialsContainer.cpp',
 ]
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
 FINAL_LIBRARY = 'xul'
 
 MOCHITEST_MANIFESTS += ['tests/mochitest/mochitest.ini']
+BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
new file mode 100644
--- /dev/null
+++ b/dom/credentialmanagement/tests/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+  "extends": [
+    "plugin:mozilla/browser-test"
+  ]
+};
new file mode 100644
--- /dev/null
+++ b/dom/credentialmanagement/tests/browser/browser.ini
@@ -0,0 +1,1 @@
+[browser_active_document.js]
new file mode 100644
--- /dev/null
+++ b/dom/credentialmanagement/tests/browser/browser_active_document.js
@@ -0,0 +1,133 @@
+/* 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/. */
+
+"use strict";
+
+const TEST_URL = "https://example.com/";
+
+function arrivingHereIsBad(aResult) {
+  ok(false, "Bad result! Received a: " + aResult);
+}
+
+function expectNotAllowedError(aResult) {
+  let expected = "NotAllowedError";
+  is(aResult.slice(0, expected.length), expected, `Expecting a ${expected}`);
+}
+
+function promiseMakeCredential(tab) {
+  return ContentTask.spawn(tab.linkedBrowser, null, async function() {
+    const cose_alg_ECDSA_w_SHA256 = -7;
+
+    let publicKey = {
+      rp: {id: content.document.domain, name: "none", icon: "none"},
+      user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
+      challenge: content.crypto.getRandomValues(new Uint8Array(16)),
+      timeout: 5000, // the minimum timeout is actually 15 seconds
+      pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}],
+    };
+
+    return content.navigator.credentials.create({publicKey});
+  });
+}
+
+function promiseGetAssertion(tab) {
+  return ContentTask.spawn(tab.linkedBrowser, null, async function() {
+    let newCredential = {
+      type: "public-key",
+      id: content.crypto.getRandomValues(new Uint8Array(16)),
+      transports: ["usb"],
+    };
+
+    let publicKey = {
+      challenge: content.crypto.getRandomValues(new Uint8Array(16)),
+      timeout: 5000, // the minimum timeout is actually 15 seconds
+      rpId: content.document.domain,
+      allowCredentials: [newCredential]
+    };
+
+    return content.navigator.credentials.get({publicKey});
+  });
+}
+
+add_task(async function test_setup() {
+  await SpecialPowers.pushPrefEnv({
+    "set": [
+      ["security.webauth.webauthn", true],
+      ["security.webauth.webauthn_enable_softtoken", true],
+      ["security.webauth.webauthn_enable_usbtoken", false]
+    ]
+  });
+});
+
+add_task(async function test_background_tab() {
+  // Open two tabs, the last one will selected.
+  let tab_bg = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+  let tab_fg = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+  // Requests from background tabs must fail.
+  await promiseMakeCredential(tab_bg)
+    .then(arrivingHereIsBad)
+    .catch(expectNotAllowedError);
+
+  // Requests from background tabs must fail.
+  await promiseGetAssertion(tab_bg)
+    .then(arrivingHereIsBad)
+    .catch(expectNotAllowedError);
+
+  // Close tabs.
+  await BrowserTestUtils.removeTab(tab_bg);
+  await BrowserTestUtils.removeTab(tab_fg);
+});
+
+add_task(async function test_background_window() {
+  // Open a tab, then a new window.
+  let tab_bg = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+  let win = await BrowserTestUtils.openNewBrowserWindow();
+
+  // Requests from selected tabs not in the active window must fail.
+  await promiseMakeCredential(tab_bg)
+    .then(arrivingHereIsBad)
+    .catch(expectNotAllowedError);
+
+  // Requests from selected tabs not in the active window must fail.
+  await promiseGetAssertion(tab_bg)
+    .then(arrivingHereIsBad)
+    .catch(expectNotAllowedError);
+
+  // Close tab and window.
+  await BrowserTestUtils.closeWindow(win);
+  await BrowserTestUtils.removeTab(tab_bg);
+});
+
+add_task(async function test_minimized() {
+  let env = Cc["@mozilla.org/process/environment;1"]
+              .getService(Ci.nsIEnvironment);
+  // Minimizing windows doesn't supported in headless mode.
+  if (env.get("MOZ_HEADLESS")) {
+    return;
+  }
+
+  // Open a window with a tab.
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+  // Minimize the window.
+  window.minimize();
+  await TestUtils.waitForCondition(() => !tab.linkedBrowser.docShellIsActive);
+
+  // Requests from minimized windows must fail.
+  await promiseMakeCredential(tab)
+    .then(arrivingHereIsBad)
+    .catch(expectNotAllowedError);
+
+  // Requests from minimized windows must fail.
+  await promiseGetAssertion(tab)
+    .then(arrivingHereIsBad)
+    .catch(expectNotAllowedError);
+
+  // Restore the window.
+  await new Promise(resolve => SimpleTest.waitForFocus(resolve, window));
+
+  // Close tab.
+  await BrowserTestUtils.removeTab(tab);
+});
--- a/dom/webauthn/WebAuthnManagerBase.cpp
+++ b/dom/webauthn/WebAuthnManagerBase.cpp
@@ -2,20 +2,23 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "mozilla/dom/WebAuthnManagerBase.h"
 #include "mozilla/dom/WebAuthnTransactionChild.h"
 #include "mozilla/dom/Event.h"
+#include "nsGlobalWindowInner.h"
+#include "nsPIWindowRoot.h"
 
 namespace mozilla {
 namespace dom {
 
+NS_NAMED_LITERAL_STRING(kDeactivateEvent, "deactivate");
 NS_NAMED_LITERAL_STRING(kVisibilityChange, "visibilitychange");
 
 WebAuthnManagerBase::WebAuthnManagerBase(nsPIDOMWindowInner* aParent)
   : mParent(aParent)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aParent);
 }
@@ -68,61 +71,86 @@ WebAuthnManagerBase::ActorDestroyed()
  * Event Handling
  **********************************************************************/
 
 void
 WebAuthnManagerBase::ListenForVisibilityEvents()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  nsCOMPtr<nsIDocument> doc = mParent->GetExtantDoc();
-  if (NS_WARN_IF(!doc)) {
+  nsCOMPtr<nsPIDOMWindowOuter> outer = mParent->GetOuterWindow();
+  if (NS_WARN_IF(!outer)) {
     return;
   }
 
-  nsresult rv = doc->AddSystemEventListener(kVisibilityChange, this,
-                                            /* use capture */ true,
-                                            /* wants untrusted */ false);
+  nsCOMPtr<EventTarget> windowRoot = outer->GetTopWindowRoot();
+  if (NS_WARN_IF(!windowRoot)) {
+    return;
+  }
+
+  nsresult rv = windowRoot->AddEventListener(kDeactivateEvent, this,
+                                             /* use capture */ true,
+                                             /* wants untrusted */ false);
+  Unused << NS_WARN_IF(NS_FAILED(rv));
+
+  rv = windowRoot->AddEventListener(kVisibilityChange, this,
+                                    /* use capture */ true,
+                                    /* wants untrusted */ false);
   Unused << NS_WARN_IF(NS_FAILED(rv));
 }
 
 void
 WebAuthnManagerBase::StopListeningForVisibilityEvents()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  nsCOMPtr<nsIDocument> doc = mParent->GetExtantDoc();
-  if (NS_WARN_IF(!doc)) {
+  nsCOMPtr<nsPIDOMWindowOuter> outer = mParent->GetOuterWindow();
+  if (NS_WARN_IF(!outer)) {
     return;
   }
 
-  nsresult rv = doc->RemoveSystemEventListener(kVisibilityChange, this,
-                                               /* use capture */ true);
+  nsCOMPtr<EventTarget> windowRoot = outer->GetTopWindowRoot();
+  if (NS_WARN_IF(!windowRoot)) {
+    return;
+  }
+
+  nsresult rv = windowRoot->RemoveEventListener(kDeactivateEvent, this,
+                                                /* use capture */ true);
+  Unused << NS_WARN_IF(NS_FAILED(rv));
+
+  rv = windowRoot->RemoveEventListener(kVisibilityChange, this,
+                                       /* use capture */ true);
   Unused << NS_WARN_IF(NS_FAILED(rv));
 }
 
 NS_IMETHODIMP
 WebAuthnManagerBase::HandleEvent(nsIDOMEvent* aEvent)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aEvent);
 
   nsAutoString type;
   aEvent->GetType(type);
-  if (!type.Equals(kVisibilityChange)) {
+  if (!type.Equals(kDeactivateEvent) && !type.Equals(kVisibilityChange)) {
     return NS_ERROR_FAILURE;
   }
 
-  nsCOMPtr<nsIDocument> doc =
-    do_QueryInterface(aEvent->InternalDOMEvent()->GetTarget());
-  if (NS_WARN_IF(!doc)) {
-    return NS_ERROR_FAILURE;
+  // The "deactivate" event on the root window has no
+  // "current inner window" and thus GetTarget() is always null.
+  if (type.Equals(kVisibilityChange)) {
+    nsCOMPtr<nsIDocument> doc =
+      do_QueryInterface(aEvent->InternalDOMEvent()->GetTarget());
+    if (NS_WARN_IF(!doc) || !doc->Hidden()) {
+      return NS_OK;
+    }
+
+    nsGlobalWindowInner* win = nsGlobalWindowInner::Cast(doc->GetInnerWindow());
+    if (NS_WARN_IF(!win) || !win->IsTopInnerWindow()) {
+      return NS_OK;
+    }
   }
 
-  if (doc->Hidden()) {
-    CancelTransaction(NS_ERROR_ABORT);
-  }
-
+  CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
   return NS_OK;
 }
 
 }
 }
--- a/dom/webauthn/tests/browser/browser_abort_visibility.js
+++ b/dom/webauthn/tests/browser/browser_abort_visibility.js
@@ -69,25 +69,29 @@ function startGetAssertionRequest(tab) {
     }).catch(() => {
       status.value = "aborted";
     });
 
     status.value = "pending";
   });
 }
 
+add_task(async function test_setup() {
+  await SpecialPowers.pushPrefEnv({
+    "set": [
+      ["security.webauth.webauthn", true],
+      ["security.webauth.webauthn_enable_softtoken", false],
+      ["security.webauth.webauthn_enable_usbtoken", true]
+    ]
+  });
+});
 
 // Test that MakeCredential() and GetAssertion() requests
 // are aborted when the current tab loses its focus.
-add_task(async function test_abort() {
-  // Enable the USB token.
-  Services.prefs.setBoolPref("security.webauth.webauthn", true);
-  Services.prefs.setBoolPref("security.webauth.webauthn_enable_softtoken", false);
-  Services.prefs.setBoolPref("security.webauth.webauthn_enable_usbtoken", true);
-
+add_task(async function test_switch_tab() {
   // Create a new tab for the MakeCredential() request.
   let tab_create = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
 
   // Start the request.
   await startMakeCredentialRequest(tab_create);
   await assertStatus(tab_create, "pending");
 
   // Open another tab and switch to it. The first will lose focus.
@@ -100,14 +104,95 @@ add_task(async function test_abort() {
 
   // Switch back to the first tab, the get() request is aborted.
   await BrowserTestUtils.switchTab(gBrowser, tab_create);
   await waitForStatus(tab_get, "aborted");
 
   // Close tabs.
   await BrowserTestUtils.removeTab(tab_create);
   await BrowserTestUtils.removeTab(tab_get);
+});
 
-  // Cleanup.
-  Services.prefs.clearUserPref("security.webauth.webauthn");
-  Services.prefs.clearUserPref("security.webauth.webauthn_enable_softtoken");
-  Services.prefs.clearUserPref("security.webauth.webauthn_enable_usbtoken");
+add_task(async function test_new_window_make() {
+  // Create a new tab for the MakeCredential() request.
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+  // Start a MakeCredential request.
+  await startMakeCredentialRequest(tab);
+  await assertStatus(tab, "pending");
+
+  // Open a new window. The tab will lose focus.
+  let win = await BrowserTestUtils.openNewBrowserWindow();
+  await waitForStatus(tab, "aborted");
+  await BrowserTestUtils.closeWindow(win);
+
+  // Close tab.
+  await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_new_window_get() {
+  // Create a new tab for the GetAssertion() request.
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+  // Start a GetAssertion request.
+  await startGetAssertionRequest(tab);
+  await assertStatus(tab, "pending");
+
+  // Open a new window. The tab will lose focus.
+  let win = await BrowserTestUtils.openNewBrowserWindow();
+  await waitForStatus(tab, "aborted");
+  await BrowserTestUtils.closeWindow(win);
+
+  // Close tab.
+  await BrowserTestUtils.removeTab(tab);
 });
+
+add_task(async function test_minimize_make() {
+  let env = Cc["@mozilla.org/process/environment;1"]
+              .getService(Ci.nsIEnvironment);
+  // Minimizing windows doesn't supported in headless mode.
+  if (env.get("MOZ_HEADLESS")) {
+    return;
+  }
+
+  // Create a new tab for the MakeCredential() request.
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+  // Start a MakeCredential request.
+  await startMakeCredentialRequest(tab);
+  await assertStatus(tab, "pending");
+
+  // Minimize the window.
+  window.minimize();
+  await waitForStatus(tab, "aborted");
+
+  // Restore the window.
+  await new Promise(resolve => SimpleTest.waitForFocus(resolve, window));
+
+  // Close tab.
+  await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_minimize_get() {
+  let env = Cc["@mozilla.org/process/environment;1"]
+              .getService(Ci.nsIEnvironment);
+  // Minimizing windows doesn't supported in headless mode.
+  if (env.get("MOZ_HEADLESS")) {
+    return;
+  }
+
+  // Create a new tab for the GetAssertion() request.
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+  // Start a GetAssertion request.
+  await startGetAssertionRequest(tab);
+  await assertStatus(tab, "pending");
+
+  // Minimize the window.
+  window.minimize();
+  await waitForStatus(tab, "aborted");
+
+  // Restore the window.
+  await new Promise(resolve => SimpleTest.waitForFocus(resolve, window));
+
+  // Close tab.
+  await BrowserTestUtils.removeTab(tab);
+});