Bug 1407789 - Prohibit cross-site iframes for Credential Management r=baku,keeler,ttaubert
authorJ.C. Jones <jjones@mozilla.com>
Thu, 12 Oct 2017 18:18:39 -0700
changeset 452565 d67a47719c805b8db375d6708f08a7b0f8335976
parent 452564 6577b2a480dc6e736b98871e0f17d66508d4d2ed
child 452566 96fefb68548f4da89ce0dcc50ec27822cb2515fb
push id1648
push usermtabara@mozilla.com
push dateThu, 01 Mar 2018 12:45:47 +0000
treeherdermozilla-release@cbb9688c2eeb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku, keeler, ttaubert
bugs1407789
milestone59.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
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
dom/credentialmanagement/CredentialsContainer.cpp
dom/credentialmanagement/CredentialsContainer.h
dom/credentialmanagement/moz.build
dom/credentialmanagement/tests/.eslintrc.js
dom/credentialmanagement/tests/mochitest/frame_credman_iframes.html
dom/credentialmanagement/tests/mochitest/mochitest.ini
dom/credentialmanagement/tests/mochitest/test_credman_iframes.html
dom/webidl/CredentialManagement.webidl
--- 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;
 };