Bug 1426767 - Ignore events in password manager from documents with a null principal. r=Gijs
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Tue, 02 Oct 2018 00:59:12 -0700
changeset 439494 8900229a39b1b2a783d310cd6795a878d62c621a
parent 439493 0edeaf35b6e65757caeadee9ed6eb52feeaed0d7
child 439495 3514b960ff957d7543795626ae72ef69ec883183
push id34776
push usernerli@mozilla.com
push dateThu, 04 Oct 2018 04:03:46 +0000
treeherdermozilla-central@8b1f1ebed0f0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersGijs
bugs1426767
milestone64.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 1426767 - Ignore events in password manager from documents with a null principal. r=Gijs Differential Revision: https://phabricator.services.mozilla.com/D7387
browser/base/content/content.js
mobile/android/components/BrowserCLH.js
toolkit/components/passwordmgr/test/mochitest/mochitest.ini
toolkit/components/passwordmgr/test/mochitest/test_autofill_sandboxed.html
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -29,27 +29,43 @@ XPCOMUtils.defineLazyGetter(this, "Login
 });
 
 // NOTE: Much of this logic is duplicated in BrowserCLH.js for Android.
 addMessageListener("RemoteLogins:fillForm", function(message) {
   // intercept if ContextMenu.jsm had sent a plain object for remote targets
   message.objects.inputElement = ContextMenuChild.getTarget(global, message, "inputElement");
   LoginManagerContent.receiveMessage(message, content);
 });
+
+function shouldIgnoreLoginManagerEvent(event) {
+  // If we have a null principal then prevent any more password manager code from running and
+  // incorrectly using the document `location`.
+  return event.target.nodePrincipal.isNullPrincipal;
+}
+
 addEventListener("DOMFormHasPassword", function(event) {
+  if (shouldIgnoreLoginManagerEvent(event)) {
+    return;
+  }
   LoginManagerContent.onDOMFormHasPassword(event, content);
   let formLike = LoginFormFactory.createFromForm(event.originalTarget);
   InsecurePasswordUtils.reportInsecurePasswords(formLike);
 });
 addEventListener("DOMInputPasswordAdded", function(event) {
+  if (shouldIgnoreLoginManagerEvent(event)) {
+    return;
+  }
   LoginManagerContent.onDOMInputPasswordAdded(event, content);
   let formLike = LoginFormFactory.createFromField(event.originalTarget);
   InsecurePasswordUtils.reportInsecurePasswords(formLike);
 });
 addEventListener("DOMAutoComplete", function(event) {
+  if (shouldIgnoreLoginManagerEvent(event)) {
+    return;
+  }
   LoginManagerContent.onUsernameInput(event);
 });
 
 ContentMetaHandler.init(this);
 
 // This is a temporary hack to prevent regressions (bug 1471327).
 void content;
 
--- a/mobile/android/components/BrowserCLH.js
+++ b/mobile/android/components/BrowserCLH.js
@@ -178,36 +178,54 @@ BrowserCLH.prototype = {
   },
 
   _initLoginManagerEvents: function(aWindow) {
     if (Services.prefs.getBoolPref("reftest.remote", false)) {
       // XXX known incompatibility between reftest harness and form-fill.
       return;
     }
 
+    function shouldIgnoreLoginManagerEvent(event) {
+      // If we have a null principal then prevent any more password manager code from running and
+      // incorrectly using the document `location`.
+      return event.target.nodePrincipal.isNullPrincipal;
+    }
+
     let options = {
       capture: true,
       mozSystemGroup: true,
     };
 
     // NOTE: Much of this logic is duplicated in browser/base/content/content.js
     // for desktop.
     aWindow.addEventListener("DOMFormHasPassword", event => {
+      if (shouldIgnoreLoginManagerEvent(event)) {
+        return;
+      }
       this.LoginManagerContent.onDOMFormHasPassword(event, event.target.ownerGlobal.top);
     }, options);
 
     aWindow.addEventListener("DOMInputPasswordAdded", event => {
+      if (shouldIgnoreLoginManagerEvent(event)) {
+        return;
+      }
       this.LoginManagerContent.onDOMInputPasswordAdded(event, event.target.ownerGlobal.top);
     }, options);
 
     aWindow.addEventListener("DOMAutoComplete", event => {
+      if (shouldIgnoreLoginManagerEvent(event)) {
+        return;
+      }
       this.LoginManagerContent.onUsernameInput(event);
     }, options);
 
     aWindow.addEventListener("blur", event => {
+      if (shouldIgnoreLoginManagerEvent(event)) {
+        return;
+      }
       if (ChromeUtils.getClassName(event.target) === "HTMLInputElement") {
         this.LoginManagerContent.onUsernameInput(event);
       }
     }, options);
 
     aWindow.addEventListener("pageshow", event => {
       // XXXbz what about non-HTML documents??
       if (ChromeUtils.getClassName(event.target) == "HTMLDocument") {
--- a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
+++ b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
@@ -3,24 +3,27 @@ support-files =
   ../../../prompts/test/chromeScript.js
   ../../../prompts/test/prompt_common.js
   ../../../satchel/test/parent_utils.js
   ../../../satchel/test/satchel_common.js
   ../authenticate.sjs
   ../blank.html
   ../browser/form_autofocus_js.html
   ../browser/form_basic.html
+  ../browser/formless_basic.html
   ../browser/form_cross_origin_secure_action.html
   ../pwmgr_common.js
   auth2/authenticate.sjs
 
 [test_autocomplete_https_upgrade.html]
 skip-if = toolkit == 'android' # autocomplete
 [test_autofill_https_upgrade.html]
 skip-if = toolkit == 'android' # Bug 1259768
+[test_autofill_sandboxed.html]
+scheme = https
 [test_autofill_password-only.html]
 [test_autofocus_js.html]
 skip-if = toolkit == 'android' # autocomplete
 [test_basic_form.html]
 [test_basic_form_0pw.html]
 [test_basic_form_1pw.html]
 [test_basic_form_1pw_2.html]
 [test_basic_form_2pw_1.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_sandboxed.html
@@ -0,0 +1,134 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test form field autofill in sandboxed documents (null principal)</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
+  <script type="text/javascript" src="satchel_common.js"></script>
+  <script type="text/javascript" src="pwmgr_common.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script>
+var chromeScript = runChecksAfterCommonInit();
+
+var setupScript = runInParent(function setup() {
+  ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+  var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+                                           Ci.nsILoginInfo, "init");
+  assert.ok(nsLoginInfo != null, "nsLoginInfo constructor");
+
+  var login1 = new nsLoginInfo("https://example.com", "", null,
+                               "tempuser1", "temppass1", "uname", "pword");
+
+  // try/catch in case someone runs the tests manually, twice.
+  try {
+    Services.logins.addLogin(login1);
+  } catch (e) {
+    assert.ok(false, "addLogin threw: " + e);
+  }
+});
+</script>
+<p id="display"></p>
+
+<div id="content">
+  <iframe id="sandboxed"
+          sandbox=""></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: form field autofill in sandboxed documents (null principal) **/
+
+let sandboxed = document.getElementById("sandboxed");
+let uname;
+let pword;
+
+// Check for expected username/password in form.
+function checkACForm(expectedUsername, expectedPassword) {
+  var formID = uname.parentNode.id;
+  is(uname.value, expectedUsername, "Checking " + formID + " username is: " + expectedUsername);
+  is(pword.value, expectedPassword, "Checking " + formID + " password is: " + expectedPassword);
+}
+
+function promiseExecuteSoon() {
+  return new Promise(SimpleTest.executeSoon);
+}
+
+add_task(async function setup() {
+  await SpecialPowers.pushPrefEnv({"set": [
+    ["security.insecure_field_warning.contextual.enabled", true],
+  ]});
+});
+
+add_task(async function test_no_autofill_in_form() {
+  sandboxed.src = "form_basic.html";
+  let frameWindow = SpecialPowers.wrap(sandboxed).contentWindow;
+  let DOMFormHasPasswordPromise = new Promise(resolve => {
+    SpecialPowers.addChromeEventListener("DOMFormHasPassword", function onDFHP() {
+      SpecialPowers.removeChromeEventListener("DOMFormHasPassword", onDFHP);
+      resolve();
+    });
+  });
+  // Can't use SimpleTest.promiseFocus as it doesn't work with the sandbox.
+  await SimpleTest.promiseWaitForCondition(() => {
+    return frameWindow.document.readyState == "complete" &&
+             frameWindow.location.href.endsWith("form_basic.html");
+  }, "Check frame is loaded");
+  info("frame loaded");
+  await DOMFormHasPasswordPromise;
+  let frameDoc = SpecialPowers.wrap(sandboxed).contentDocument;
+
+  uname = frameDoc.getElementById("form-basic-username");
+  pword = frameDoc.getElementById("form-basic-password");
+
+  await promiseExecuteSoon();
+  // Autofill shouldn't happen in the sandboxed frame but would have happened by
+  // now since DOMFormHasPassword was observed above.
+  checkACForm("", "");
+
+  info("blurring the username field after typing the username");
+  uname.focus();
+  uname.setUserInput("tempuser1");
+  synthesizeKey("VK_TAB", {}, frameWindow);
+  await promiseExecuteSoon();
+  await promiseExecuteSoon();
+  await promiseExecuteSoon();
+  checkACForm("tempuser1", "");
+});
+
+add_task(async function test_no_autofill_outside_form() {
+  sandboxed.src = "formless_basic.html";
+  let frameWindow = SpecialPowers.wrap(sandboxed).contentWindow;
+  let DOMInputPasswordAddedPromise = new Promise(resolve => {
+    SpecialPowers.addChromeEventListener("DOMInputPasswordAdded", function onDIPA() {
+      SpecialPowers.removeChromeEventListener("DOMInputPasswordAdded", onDIPA);
+      resolve();
+    });
+  });
+  // Can't use SimpleTest.promiseFocus as it doesn't work with the sandbox.
+  await SimpleTest.promiseWaitForCondition(() => {
+    return frameWindow.document.readyState == "complete" &&
+             frameWindow.location.href.endsWith("formless_basic.html");
+  }, "Check frame is loaded");
+  info("frame loaded");
+  await DOMInputPasswordAddedPromise;
+  let frameDoc = SpecialPowers.wrap(sandboxed).contentDocument;
+
+  uname = frameDoc.getElementById("form-basic-username");
+  pword = frameDoc.getElementById("form-basic-password");
+
+  await promiseExecuteSoon();
+  // Autofill shouldn't happen in the sandboxed frame but would have happened by
+  // now since DOMInputPasswordAdded was observed above.
+  checkACForm("", "");
+});
+</script>
+</pre>
+</body>
+</html>