Bug 1191092 - Check password box not inside a form element.; r?MattN draft
authorSean Lee <selee@mozilla.com>
Sun, 03 Apr 2016 00:33:04 +0800
changeset 350320 27d841661cad8968dd91b6541708d017780c5624
parent 349384 e847cfcb315f511f4928b03fd47dcf57aad05e1e
child 518292 83338a4d8dbaf1cfd409e775a6a37d3fa6ddcf0f
push id15298
push userbmo:selee@mozilla.com
push dateWed, 13 Apr 2016 08:55:28 +0000
reviewersMattN
bugs1191092
milestone48.0a1
Bug 1191092 - Check password box not inside a form element.; r?MattN MozReview-Commit-ID: Q5abQmgdhA
browser/base/content/content.js
toolkit/components/passwordmgr/InsecurePasswordUtils.jsm
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/test/browser/browser.ini
toolkit/components/passwordmgr/test/browser/browser_insecurePasswordWarning.js
toolkit/components/passwordmgr/test/browser/formless_basic.html
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -19,16 +19,18 @@ Cu.import("resource://gre/modules/Task.j
 XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
   "resource:///modules/E10SUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
   "resource://gre/modules/BrowserUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ContentLinkHandler",
   "resource:///modules/ContentLinkHandler.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
   "resource://gre/modules/LoginManagerContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormLikeFactory",
+  "resource://gre/modules/LoginManagerContent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
   "resource://gre/modules/InsecurePasswordUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PluginContent",
   "resource:///modules/PluginContent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormSubmitObserver",
   "resource:///modules/FormSubmitObserver.jsm");
@@ -56,20 +58,23 @@ addMessageListener("ContextMenu:DoCustom
     () => PageMenuChild.executeMenu(message.data.generatedItemId));
 });
 
 addMessageListener("RemoteLogins:fillForm", function(message) {
   LoginManagerContent.receiveMessage(message, content);
 });
 addEventListener("DOMFormHasPassword", function(event) {
   LoginManagerContent.onDOMFormHasPassword(event, content);
-  InsecurePasswordUtils.checkForInsecurePasswords(event.target);
+  let formLike = FormLikeFactory.createFromForm(event.target);
+  InsecurePasswordUtils.checkForInsecurePasswords(formLike);
 });
 addEventListener("DOMInputPasswordAdded", function(event) {
   LoginManagerContent.onDOMInputPasswordAdded(event, content);
+  let formLike = FormLikeFactory.createFromField(event.target);
+  InsecurePasswordUtils.checkForInsecurePasswords(formLike);
 });
 addEventListener("pageshow", function(event) {
   LoginManagerContent.onPageShow(event, content);
 });
 addEventListener("DOMAutoComplete", function(event) {
   LoginManagerContent.onUsernameInput(event);
 });
 addEventListener("blur", function(event) {
--- a/toolkit/components/passwordmgr/InsecurePasswordUtils.jsm
+++ b/toolkit/components/passwordmgr/InsecurePasswordUtils.jsm
@@ -17,16 +17,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyGetter(this, "WebConsoleUtils", () => {
   return this.devtools.require("devtools/shared/webconsole/utils").Utils;
 });
 XPCOMUtils.defineLazyGetter(this, "l10n", () => {
   return new this.WebConsoleUtils.L10n(STRINGS_URI);
 });
 
 this.InsecurePasswordUtils = {
+  _formRootsWarned: new WeakMap(),
   _sendWebConsoleMessage(messageTag, domDoc) {
     let windowId = WebConsoleUtils.getInnerWindowId(domDoc.defaultView);
     let category = "Insecure Password Field";
     // All web console messages are warnings for now.
     let flag = Ci.nsIScriptError.warningFlag;
     let message = l10n.getStr(messageTag);
     let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
     consoleMsg.initWithWindowID(message, domDoc.location.href, 0, 0, 0, flag, category, windowId);
@@ -64,40 +65,50 @@ this.InsecurePasswordUtils = {
    * Checks if there are insecure password fields present on the form's document
    * i.e. passwords inside forms with http action, inside iframes with http src,
    * or on insecure web pages. If insecure password fields are present,
    * a log message is sent to the web console to warn developers.
    *
    * @param {FormLike} aForm A form-like object. @See {FormLikeFactory}
    */
   checkForInsecurePasswords(aForm) {
+    if (this._formRootsWarned.has(aForm.rootElement) ||
+        this._formRootsWarned.get(aForm.rootElement)) {
+      return;
+    }
+
     let domDoc = aForm.ownerDocument;
     let topDocument = domDoc.defaultView.top.document;
     let isSafePage = LoginManagerContent.isDocumentSecure(topDocument);
 
     if (!isSafePage) {
       this._sendWebConsoleMessage("InsecurePasswordsPresentOnPage", domDoc);
+      this._formRootsWarned.set(aForm.rootElement, true);
     }
 
     // Check if we are on an iframe with insecure src, or inside another
     // insecure iframe or document.
     if (this._checkForInsecureNestedDocuments(domDoc)) {
       this._sendWebConsoleMessage("InsecurePasswordsPresentOnIframe", domDoc);
+      this._formRootsWarned.set(aForm.rootElement, true);
       isSafePage = false;
     }
 
     let isFormSubmitHTTP = false, isFormSubmitHTTPS = false;
-    // Note that aForm.action can be a relative path (e.g. "", "/login", "//example.com", etc.)
-    // but we don't warn about those since we would have already warned about the form's document
-    // not being safe above.
-    if (aForm.action.match(/^http:\/\//)) {
-      this._sendWebConsoleMessage("InsecureFormActionPasswordsPresent", domDoc);
-      isFormSubmitHTTP = true;
-    } else if (aForm.action.match(/^https:\/\//)) {
-      isFormSubmitHTTPS = true;
+    if (aForm.rootElement instanceof Ci.nsIDOMHTMLFormElement) {
+      // Note that aForm.action can be a relative path (e.g. "", "/login", "//example.com", etc.)
+      // but we don't warn about those since we would have already warned about the form's document
+      // not being safe above.
+      if (aForm.action.match(/^http:\/\//)) {
+        this._sendWebConsoleMessage("InsecureFormActionPasswordsPresent", domDoc);
+        this._formRootsWarned.set(aForm.rootElement, true);
+        isFormSubmitHTTP = true;
+      } else if (aForm.action.match(/^https:\/\//)) {
+        isFormSubmitHTTPS = true;
+      }
     }
 
     // The safety of a password field determined by the form action and the page protocol
     let passwordSafety;
     if (isSafePage) {
       if (isFormSubmitHTTPS) {
         passwordSafety = 0;
       } else if (isFormSubmitHTTP) {
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -1,15 +1,16 @@
 /* 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/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [ "LoginManagerContent",
+                          "FormLikeFactory",
                           "UserAutoCompleteResult" ];
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 const PASSWORD_INPUT_ADDED_COALESCING_THRESHOLD_MS = 1;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
--- a/toolkit/components/passwordmgr/test/browser/browser.ini
+++ b/toolkit/components/passwordmgr/test/browser/browser.ini
@@ -9,28 +9,30 @@ support-files =
   ../subtst_notifications_4.html
   ../subtst_notifications_5.html
   ../subtst_notifications_6.html
   ../subtst_notifications_8.html
   ../subtst_notifications_9.html
   ../subtst_notifications_10.html
   authenticate.sjs
   form_basic.html
+  formless_basic.html
   head.js
   insecure_test.html
   insecure_test_subframe.html
   multiple_forms.html
   streamConverter_content.sjs
 
 [browser_capture_doorhanger.js]
 [browser_DOMFormHasPassword.js]
 [browser_DOMInputPasswordAdded.js]
 [browser_filldoorhanger.js]
 [browser_hasInsecureLoginForms.js]
 [browser_hasInsecureLoginForms_streamConverter.js]
+[browser_insecurePasswordWarning.js]
 [browser_notifications.js]
 skip-if = true # Intermittent failures: Bug 1182296, bug 1148771
 [browser_passwordmgr_editing.js]
 skip-if = os == "linux"
 [browser_context_menu.js]
 skip-if = e10s
 [browser_passwordmgr_contextmenu.js]
 [browser_passwordmgr_fields.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_insecurePasswordWarning.js
@@ -0,0 +1,84 @@
+"use strict";
+
+const TEST_URL_PATH = "/browser/toolkit/components/passwordmgr/test/browser/";
+
+const WARNING_PATTERN = [{
+  key: "INSECURE_FORM_ACTION",
+  msg: 'JavaScript Warning: "Password fields present in a form with an insecure (http://) form action. This is a security risk that allows user login credentials to be stolen."'
+}, {
+  key: "INSECURE_PAGE",
+  msg: 'JavaScript Warning: "Password fields present on an insecure (http://) page. This is a security risk that allows user login credentials to be stolen."'
+}];
+
+add_task(function* testInsecurePasswordWarning() {
+  let warningPatternHandler;
+
+  function messageHandler(msg) {
+    function findWarningPattern(msg) {
+      return WARNING_PATTERN.find(patternPair => {
+        return msg.indexOf(patternPair.msg) !== -1;
+      });
+    }
+
+    let warning = findWarningPattern(msg.message);
+
+    // Only handle the insecure password related warning messages.
+    if (warning) {
+      // Prevent any unexpected or redundant matched warning message coming after
+      // the test case is ended.
+      ok(warningPatternHandler, "Invoke a valid warning message handler");
+      warningPatternHandler(warning, msg.message);
+    }
+  }
+  Services.console.registerListener(messageHandler);
+  registerCleanupFunction(function() {
+    Services.console.unregisterListener(messageHandler);
+  });
+
+  for (let [origin, testFile, expectWarnings] of [
+    // Form action at 127.0.0.1/localhost is considered as a secure case.
+    // There should be no INSECURE_FORM_ACTION warning at 127.0.0.1/localhost.
+    // This will be fixed at Bug 1261234.
+    ["http://127.0.0.1", "form_basic.html", ["INSECURE_FORM_ACTION"]],
+    ["http://127.0.0.1", "formless_basic.html", []],
+    ["http://example.com", "form_basic.html", ["INSECURE_FORM_ACTION", "INSECURE_PAGE"]],
+    ["http://example.com", "formless_basic.html", ["INSECURE_PAGE"]],
+    ["https://example.com", "form_basic.html", []],
+    ["https://example.com", "formless_basic.html", []],
+  ]) {
+    let testUrlPath = origin + TEST_URL_PATH + testFile;
+    var promiseConsoleMessages = new Promise(resolve => {
+      warningPatternHandler = function (warning, originMessage) {
+        ok(warning, "Handling a warning pattern");
+        let fullMessage = `[${warning.msg} {file: "${testUrlPath}" line: 0 column: 0 source: "0"}]`;
+        is(originMessage, fullMessage, "Message full matched:" + originMessage);
+
+        let index = expectWarnings.indexOf(warning.key);
+        isnot(index, -1, "Found warning: " + warning.key + " for URL:" + testUrlPath);
+        if (index !== -1) {
+          // Remove the shown message.
+          expectWarnings.splice(index, 1);
+        }
+        if (expectWarnings.length === 0) {
+          info("All warnings are shown. URL:" + testUrlPath);
+          resolve();
+        }
+      }
+    });
+
+    yield BrowserTestUtils.withNewTab({
+      gBrowser,
+      url: testUrlPath
+    }, function*() {
+      if (expectWarnings.length === 0) {
+        info("All warnings are shown. URL:" + testUrlPath);
+        return Promise.resolve();
+      }
+      return promiseConsoleMessages;
+    });
+
+    // Remove warningPatternHandler to stop handling the matched warning pattern
+    // and the task should not get any warning anymore.
+    warningPatternHandler = null;
+  }
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/formless_basic.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+
+<!-- Simplest form with username and password fields. -->
+  <input id="form-basic-username" name="username">
+  <input id="form-basic-password" name="password" type="password">
+  <input id="form-basic-submit" type="submit">
+
+  <button id="add">Add input[type=password]</button>
+
+  <script>
+    document.getElementById("add").addEventListener("click", function () {
+      var node = document.createElement("input");
+      node.setAttribute("type", "password");
+      document.querySelector("body").appendChild(node);
+    });
+  </script>
+
+</body></html>