mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-09 11:25:00 +00:00
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:
parent
6c8bcd4625
commit
afe259f21f
@ -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);
|
||||
}
|
||||
|
@ -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']
|
||||
|
7
dom/credentialmanagement/tests/browser/.eslintrc.js
Normal file
7
dom/credentialmanagement/tests/browser/.eslintrc.js
Normal file
@ -0,0 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
"extends": [
|
||||
"plugin:mozilla/browser-test"
|
||||
]
|
||||
};
|
1
dom/credentialmanagement/tests/browser/browser.ini
Normal file
1
dom/credentialmanagement/tests/browser/browser.ini
Normal file
@ -0,0 +1 @@
|
||||
[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);
|
||||
});
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user