Bug 1407789 - Prohibit iframes for Credential Management r?qdot r?ttaubert draft
authorJ.C. Jones <jjones@mozilla.com>
Thu, 12 Oct 2017 18:18:39 -0700
changeset 680410 99a0e73bc99c502af9bf6cacbb5203055eaa40b9
parent 679509 98247fbf95c260786361e12ad924c4370885f686
child 735853 803c9e995fc563256b0540c8d221b66ebf548738
push id84501
push userbmo:jjones@mozilla.com
push dateSat, 14 Oct 2017 01:06:38 +0000
reviewersqdot, ttaubert
bugs1407789
milestone58.0a1
Bug 1407789 - Prohibit iframes for Credential Management r?qdot r?ttaubert Credential Management defines that if the responsible document is not the active document in a top-level browsing context, then operations should return NotSupportedError [1][2]. However, we have to also allow iframes from the same origin for Mochitests, and it's just a reasonable expansion that's been discussed in-person a number of times. This ensures that https://webauthn.bin.coffee/iframe.html works, but the cross-origin https://u2f.bin.coffee/iframe-webauthn.html does not. qdot: Please check my logic for IsNotInAFrameOrIsSameOrigin - it appears to work, but I'm no DOM expert! [1] https://w3c.github.io/webappsec-credential-management/#algorithm-request [2] https://w3c.github.io/webappsec-credential-management/#algorithm-create MozReview-Commit-ID: KIyakgl0kGv
dom/credentialmanagement/CredentialsContainer.cpp
dom/credentialmanagement/moz.build
dom/credentialmanagement/tests/mochitest/frame_credman_iframes.html
dom/credentialmanagement/tests/mochitest/mochitest.ini
dom/credentialmanagement/tests/mochitest/test_credman_iframes.html
--- a/dom/credentialmanagement/CredentialsContainer.cpp
+++ b/dom/credentialmanagement/CredentialsContainer.cpp
@@ -2,28 +2,87 @@
 /* vim:set ts=2 sw=2 sts=2 et cindent: */
 /* 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)
 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, const nsresult& aResult)
+{
+  MOZ_ASSERT(aParent);
+  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aParent);
+  if (NS_WARN_IF(!global)) {
+    return nullptr;
+  }
+
+  ErrorResult rv;
+  RefPtr<Promise> promise = Promise::Create(global, rv);
+  if(NS_WARN_IF(rv.Failed())) {
+    return nullptr;
+  }
+
+  promise->MaybeReject(aResult);
+  return promise.forget();
+}
+
+bool
+IsNotInAFrameOrIsSameOrigin(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
+  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aParent);
+
+  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()
 {}
@@ -32,21 +91,29 @@ JSObject*
 CredentialsContainer::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return CredentialsContainerBinding::Wrap(aCx, this, aGivenProto);
 }
 
 already_AddRefed<Promise>
 CredentialsContainer::Get(const CredentialRequestOptions& aOptions)
 {
+  if (!IsNotInAFrameOrIsSameOrigin(mParent)) {
+    return CreateAndReject(mParent, NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+  }
+
   RefPtr<WebAuthnManager> mgr = WebAuthnManager::GetOrCreate();
   return mgr->GetAssertion(mParent, aOptions.mPublicKey);
 }
 
 already_AddRefed<Promise>
 CredentialsContainer::Create(const CredentialCreationOptions& aOptions)
 {
+  if (!IsNotInAFrameOrIsSameOrigin(mParent)) {
+    return CreateAndReject(mParent, NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+  }
+
   RefPtr<WebAuthnManager> mgr = WebAuthnManager::GetOrCreate();
   return mgr->MakeCredential(mParent, aOptions.mPublicKey);
 }
 
 } // namespace dom
 } // namespace mozilla
--- 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/mochitest/frame_credman_iframes.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<html>
+<head>
+  <title>Embedded Frame for Credential Management: Prohibit use in iframes</title>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+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("NotSupportedError"), "Expecting a NotSupportedError, but received " + aResult);
+  })
+  .then(function() {
+    local_finished();
+  });
+}
+
+let rp = {id: document.domain, name: "none", icon: "none"};
+let user = {name: "none", icon: "none", displayName: "none"};
+let params = [ {type: "public-key", alg: "es256"}, {type: "public-key", alg: -7} ];
+let makeCredentialOptions = {
+  rp: rp, user: user, challenge: new Uint8Array(), pubKeyCredParams: params
+};
+
+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,56 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+  <title>Credential Management: Prohibit use in 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" />
+</head>
+<body>
+<h1>Credential Management: Prohibit use in 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">
+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) {
+      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>