Bug 1407789 - Prohibit cross-site iframes for Credential Management r=baku,keeler,ttaubert

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

--HG--
extra : rebase_source : dace4f4d73823913bff759fce8255da8e18ad5e3
This commit is contained in:
J.C. Jones 2017-10-12 18:18:39 -07:00
Родитель 102f4d3a4b
Коммит bce88244c0
8 изменённых файлов: 251 добавлений и 6 удалений

Просмотреть файл

@ -7,6 +7,7 @@
#include "mozilla/dom/CredentialsContainer.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/WebAuthnManager.h"
#include "nsContentUtils.h"
namespace mozilla {
namespace dom {
@ -19,6 +20,63 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CredentialsContainer)
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)
{
@ -45,22 +103,42 @@ CredentialsContainer::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenPro
}
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);
}

Просмотреть файл

@ -33,13 +33,13 @@ public:
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();

Просмотреть файл

@ -20,3 +20,5 @@ UNIFIED_SOURCES += [
include('/ipc/chromium/chromium-config.mozbuild')
FINAL_LIBRARY = 'xul'
MOCHITEST_MANIFESTS += ['tests/mochitest/mochitest.ini']

Просмотреть файл

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

Просмотреть файл

@ -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>

Просмотреть файл

@ -0,0 +1,7 @@
[DEFAULT]
support-files =
frame_credman_iframes.html
scheme = https
skip-if = !e10s
[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>

Просмотреть файл

@ -15,8 +15,11 @@ interface Credential {
[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);
};