Bug 1407789 - Prohibit cross-site iframes for Credential Management r?baku r?keeler
Credential Management defines a parameter `sameOriginWithAncestors` which is
set true if the responsible document is not either in a top-level browsing
context, or is in a nested context whose heirarchy is all loaded from the
same origin as the top-level context [1][2]. The individual credential types
of CredMan can use this flag to make decisions on whether to error or not.
Our Credential Management implementation right now is a shim to Web
Authentication, which says that if `sameOriginWithAncestors` is false, return
`"NotAllowedError"`.
This ensures that
https://webauthn.bin.coffee/iframe.html
works, but the cross-origin
https://u2f.bin.coffee/iframe-webauthn.html
does not.
[1] https://w3c.github.io/webappsec-credential-management/#algorithm-request
[2] https://w3c.github.io/webappsec-credential-management/#algorithm-create
[3] https://w3c.github.io/webauthn/#createCredential
[4] https://w3c.github.io/webauthn/#getAssertion
MozReview-Commit-ID: KIyakgl0kGv
--- 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;
};