author | J.C. Jones <jjones@mozilla.com> |
Thu, 12 Oct 2017 18:18:39 -0700 | |
changeset 436952 | d67a47719c805b8db375d6708f08a7b0f8335976 |
parent 436951 | 6577b2a480dc6e736b98871e0f17d66508d4d2ed |
child 436953 | 96fefb68548f4da89ce0dcc50ec27822cb2515fb |
push id | unknown |
push user | unknown |
push date | unknown |
reviewers | baku, keeler, ttaubert |
bugs | 1407789 |
milestone | 59.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
|
--- a/dom/credentialmanagement/CredentialsContainer.cpp +++ b/dom/credentialmanagement/CredentialsContainer.cpp @@ -2,28 +2,86 @@ /* 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/CredentialsContainer.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/WebAuthnManager.h" +#include "nsContentUtils.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) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END +already_AddRefed<Promise> +CreateAndReject(nsPIDOMWindowInner* aParent, ErrorResult& aRv) +{ + MOZ_ASSERT(aParent); + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aParent); + if (NS_WARN_IF(!global)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR); + return promise.forget(); +} + +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); + + if (aParent->IsTopInnerWindow()) { + // Not in a frame or iframe + return true; + } + + // We're in some kind of frame, so let's get the parent and start checking + // the same origin policy + nsINode* node = nsContentUtils::GetCrossDocParentNode(aParent->GetExtantDoc()); + if (NS_WARN_IF(!node)) { + // This is a sanity check, since there has to be a parent. Fail safe. + return false; + } + + // Check that all ancestors are the same origin, repeating until we find a + // null parent + do { + nsresult rv = nsContentUtils::CheckSameOrigin(aParent->GetExtantDoc(), node); + if (NS_FAILED(rv)) { + // same-origin policy is violated + return false; + } + + node = nsContentUtils::GetCrossDocParentNode(node); + } while (node); + + return true; +} + CredentialsContainer::CredentialsContainer(nsPIDOMWindowInner* aParent) : mParent(aParent) { MOZ_ASSERT(aParent); } CredentialsContainer::~CredentialsContainer() {} @@ -40,30 +98,50 @@ CredentialsContainer::EnsureWebAuthnMana JSObject* CredentialsContainer::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { return CredentialsContainerBinding::Wrap(aCx, this, aGivenProto); } already_AddRefed<Promise> -CredentialsContainer::Get(const CredentialRequestOptions& aOptions) +CredentialsContainer::Get(const CredentialRequestOptions& aOptions, + ErrorResult& aRv) { + if (!IsSameOriginWithAncestors(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) +CredentialsContainer::Create(const CredentialCreationOptions& aOptions, + ErrorResult& aRv) { + if (!IsSameOriginWithAncestors(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) +CredentialsContainer::Store(const Credential& aCredential, ErrorResult& aRv) { + if (!IsSameOriginWithAncestors(mParent)) { + return CreateAndReject(mParent, aRv); + } + + // TODO: Check that we're an active document, too. See bug 1409202. + EnsureWebAuthnManager(); return mManager->Store(aCredential); } } // namespace dom } // namespace mozilla
--- a/dom/credentialmanagement/CredentialsContainer.h +++ b/dom/credentialmanagement/CredentialsContainer.h @@ -28,23 +28,23 @@ public: { return mParent; } virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; already_AddRefed<Promise> - Get(const CredentialRequestOptions& aOptions); + Get(const CredentialRequestOptions& aOptions, ErrorResult& aRv); already_AddRefed<Promise> - Create(const CredentialCreationOptions& aOptions); + Create(const CredentialCreationOptions& aOptions, ErrorResult& aRv); already_AddRefed<Promise> - Store(const Credential& aCredential); + Store(const Credential& aCredential, ErrorResult& aRv); private: ~CredentialsContainer(); void EnsureWebAuthnManager(); nsCOMPtr<nsPIDOMWindowInner> mParent; RefPtr<WebAuthnManager> mManager;
--- a/dom/credentialmanagement/moz.build +++ b/dom/credentialmanagement/moz.build @@ -15,8 +15,10 @@ EXPORTS.mozilla.dom += [ UNIFIED_SOURCES += [ 'Credential.cpp', 'CredentialsContainer.cpp', ] include('/ipc/chromium/chromium-config.mozbuild') FINAL_LIBRARY = 'xul' + +MOCHITEST_MANIFESTS += ['tests/mochitest/mochitest.ini']
new file mode 100644 --- /dev/null +++ b/dom/credentialmanagement/tests/.eslintrc.js @@ -0,0 +1,10 @@ +"use strict"; + +module.exports = { + "extends": [ + "plugin:mozilla/mochitest-test", + ], + "plugins": [ + "mozilla" + ] +}; \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/dom/credentialmanagement/tests/mochitest/frame_credman_iframes.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<html> +<head> + <title>Embedded Frame for Credential Management: Prohibit use in cross-origin iframes</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta charset=utf-8> +</head> +<body> + +<script class="testbody" type="text/javascript"> +"use strict"; + +const cose_alg_ECDSA_w_SHA256 = -7; +var _parentOrigin = "https://example.com/"; + +function log(msg) { + console.log(msg); + let logBox = document.getElementById("log"); + if (logBox) { + logBox.textContent += "\n" + msg; + } +} + +function local_finished() { + parent.postMessage({"done": true}, _parentOrigin); + log("Done."); +} + +function local_ok(expression, message) { + let body = {"test": expression, "status": expression, "msg": message}; + parent.postMessage(body, _parentOrigin); + log(expression + ": " + message); +} + +function testSameOrigin() { + log("Same origin: " + document.domain); + + navigator.credentials.create({publicKey: makeCredentialOptions}) + .then(function sameOriginCreateThen(aResult) { + local_ok(aResult != undefined, "Create worked " + aResult); + }) + .catch(function sameOriginCatch(aResult) { + local_ok(false, "Should not have failed " + aResult); + }) + .then(function() { + local_finished(); + }); +} + +function testCrossOrigin() { + log("Cross-origin: " + document.domain); + + navigator.credentials.create({publicKey: makeCredentialOptions}) + .then(function crossOriginThen(aBad) { + local_ok(false, "Should not have succeeded " + aBad); + }) + .catch(function crossOriginCatch(aResult) { + local_ok(aResult.toString().startsWith("NotAllowedError"), + "Expecting a NotAllowedError, received " + aResult); + }) + .then(function() { + local_finished(); + }); +} + +let rp = {id: document.domain, name: "none", icon: "none"}; +let user = { + id: crypto.getRandomValues(new Uint8Array(16)), + name: "none", icon: "none", displayName: "none" +}; +let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256}; +let makeCredentialOptions = { + rp, user, challenge: new Uint8Array(), pubKeyCredParams: [param] +}; + +if (document.domain == "example.com") { + testSameOrigin(); +} else { + testCrossOrigin(); +} + +</script> + +<div id="log"></div> + +</body> +</html>
new file mode 100644 --- /dev/null +++ b/dom/credentialmanagement/tests/mochitest/mochitest.ini @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = + frame_credman_iframes.html +scheme = https +skip-if = !e10s + +[test_credman_iframes.html]
new file mode 100644 --- /dev/null +++ b/dom/credentialmanagement/tests/mochitest/test_credman_iframes.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<head> + <title>Credential Management: Prohibit use in cross-origin iframes</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta charset=utf-8> +</head> +<body> +<h1>Credential Management: Prohibit use in cross-origin iframes</h1> +<ul> + <li><a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1407789">Mozilla Bug 1407789</a></li> +</ul> + +<div id="framediv"> + <h2>Same Origin Test</h2> + <iframe id="frame_top"></iframe> + + <h2>Cross-Origin Test</h2> + <iframe id="frame_bottom"></iframe> +</div> + +<script class="testbody" type="text/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); + +var _countCompletes = 0; +var _expectedCompletes = 2; // 2 iframes + +function handleEventMessage(event) { + if ("test" in event.data) { + let summary = event.data.test + ": " + event.data.msg; + ok(event.data.status, summary); + } else if ("done" in event.data) { + _countCompletes += 1; + if (_countCompletes == _expectedCompletes) { + console.log("Test compeleted. Finished."); + SimpleTest.finish(); + } + } else { + ok(false, "Unexpected message in the test harness: " + event.data); + } +} + +window.addEventListener("message", handleEventMessage); +SpecialPowers.pushPrefEnv({"set": [["security.webauth.webauthn", true], + ["security.webauth.webauthn_enable_softtoken", true], + ["security.webauth.webauthn_enable_usbtoken", false]]}, +function() { + document.getElementById("frame_top").src = "https://example.com/tests/dom/credentialmanagement/tests/mochitest/frame_credman_iframes.html"; + + document.getElementById("frame_bottom").src = "https://test1.example.com/tests/dom/credentialmanagement/tests/mochitest/frame_credman_iframes.html"; + +}); +</script> +</body> +</html>
--- a/dom/webidl/CredentialManagement.webidl +++ b/dom/webidl/CredentialManagement.webidl @@ -10,18 +10,21 @@ [Exposed=Window, SecureContext, Pref="security.webauth.webauthn"] interface Credential { readonly attribute USVString id; readonly attribute DOMString type; }; [Exposed=Window, SecureContext, Pref="security.webauth.webauthn"] interface CredentialsContainer { + [Throws] Promise<Credential?> get(optional CredentialRequestOptions options); + [Throws] Promise<Credential?> create(optional CredentialCreationOptions options); + [Throws] Promise<Credential> store(Credential credential); }; dictionary CredentialRequestOptions { PublicKeyCredentialRequestOptions publicKey; AbortSignal signal; };