Bug 1407789 - Prohibit cross-site iframes for Credential Management r?baku r?keeler draft
authorJ.C. Jones <jjones@mozilla.com>
Thu, 12 Oct 2017 18:18:39 -0700
changeset 717331 07e993a4160f3074720c4a56f6c531ad21df0c2f
parent 717183 ca379fcca95b1f4a3744242ea8647004b99b3507
child 745217 4d34c0a073849f05212171649a47d02120c9e45b
push id94635
push userbmo:jjones@mozilla.com
push dateMon, 08 Jan 2018 17:23:45 +0000
reviewersbaku, keeler
bugs1407789
milestone59.0a1
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
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;
 };