Bug 1191092 - Warn about insecure <input type=password> outside of a <form>. r=MattN
authorSean Lee <selee@mozilla.com>
Thu, 14 Apr 2016 15:54:31 -0700
changeset 331296 41fa5b9cf8dc4961c3103398805a44133ff0b9c5
parent 331295 11524fe96945c3c2e784db6ab3ddba10dbde3c29
child 331297 a61539ca56bd03c517697c8bf873ed4f2efd1148
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1191092
milestone48.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 1191092 - Warn about insecure <input type=password> outside of a <form>. 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
@@ -10,30 +10,32 @@ support-files =
   ../subtst_notifications_5.html
   ../subtst_notifications_6.html
   ../subtst_notifications_8.html
   ../subtst_notifications_9.html
   ../subtst_notifications_10.html
   ../subtst_notifications_change_p.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_username_select_dialog.js]
 skip-if = e10s # bug 1263760
 [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>