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

--HG--
extra : amend_source : 112378a1ab2e883d7603e8a28ff3f8e944d57b5f
This commit is contained in:
Tim Taubert 2018-03-10 06:43:20 +01:00
parent 6c8bcd4625
commit afe259f21f
7 changed files with 333 additions and 39 deletions

View File

@ -8,6 +8,8 @@
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/WebAuthnManager.h"
#include "nsContentUtils.h"
#include "nsFocusManager.h"
#include "nsIDocShell.h"
namespace mozilla {
namespace dom {
@ -40,7 +42,50 @@ CreateAndReject(nsPIDOMWindowInner* aParent, ErrorResult& aRv)
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
@ -106,12 +151,10 @@ 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);
}
@ -120,12 +163,10 @@ 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);
}
@ -133,12 +174,10 @@ CredentialsContainer::Create(const CredentialCreationOptions& aOptions,
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);
}

View File

@ -22,3 +22,4 @@ include('/ipc/chromium/chromium-config.mozbuild')
FINAL_LIBRARY = 'xul'
MOCHITEST_MANIFESTS += ['tests/mochitest/mochitest.ini']
BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']

View File

@ -0,0 +1,7 @@
"use strict";
module.exports = {
"extends": [
"plugin:mozilla/browser-test"
]
};

View File

@ -0,0 +1 @@
[browser_active_document.js]

View File

@ -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);
});

View File

@ -7,10 +7,13 @@
#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)
@ -73,14 +76,24 @@ 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));
}
@ -89,13 +102,22 @@ 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));
}
@ -107,20 +129,26 @@ WebAuthnManagerBase::HandleEvent(nsIDOMEvent* 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;
}
if (doc->Hidden()) {
CancelTransaction(NS_ERROR_ABORT);
// 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;
}
}
CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
return NS_OK;
}

View File

@ -74,15 +74,19 @@ function startGetAssertionRequest(tab) {
});
}
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);
@ -105,9 +109,90 @@ add_task(async function test_abort() {
// 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);
});