Bug 1259355 - Practice MozReview; r?KuoE0 draft
authorSean Lee <weilonge@gmail.com>
Mon, 21 Mar 2016 11:36:20 +0800
changeset 345214 1d05d17e0cf031d1e5031ed00021bd15f392bce9
parent 342671 f14898695ee0dd14615914f3e1401f17df57fdd7
child 517129 2d55e9bef7d91cdb04938c1617ed1f0cab621114
push id14019
push userbmo:selee@mozilla.com
push dateMon, 28 Mar 2016 09:57:14 +0000
reviewersKuoE0
bugs1259355
milestone48.0a1
Bug 1259355 - Practice MozReview; r?KuoE0 MozReview-Commit-ID: 4XHXuezG7HY
browser/base/content/content.js
toolkit/components/passwordmgr/InsecurePasswordUtils.jsm
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/moz.build
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
@@ -17,16 +17,18 @@ Cu.import("resource://gre/modules/Inline
 Cu.import("resource://gre/modules/Task.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");
@@ -52,20 +54,23 @@ addMessageListener("ContextMenu:DoCustom
   PageMenuChild.executeMenu(message.data);
 });
 
 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, "", 0, 0, 0, flag, category, windowId);
@@ -64,37 +65,47 @@ 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;
-    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) {
+      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/moz.build
+++ b/toolkit/components/passwordmgr/moz.build
@@ -10,16 +10,17 @@ if CONFIG['MOZ_BUILD_APP'] == 'browser':
 MOCHITEST_MANIFESTS += ['test/mochitest.ini', 'test/mochitest/mochitest.ini']
 MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini']
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 
 TESTING_JS_MODULES += [
     # Make this file available from the "resource:" URI of the test environment.
     'test/browser/form_basic.html',
+    'test/browser/formless_basic.html',
     'test/LoginTestUtils.jsm',
 ]
 
 XPIDL_SOURCES += [
     'nsILoginInfo.idl',
     'nsILoginManager.idl',
     'nsILoginManagerCrypto.idl',
     'nsILoginManagerPrompter.idl',
--- a/toolkit/components/passwordmgr/test/browser/browser.ini
+++ b/toolkit/components/passwordmgr/test/browser/browser.ini
@@ -1,22 +1,24 @@
 [DEFAULT]
 support-files =
   authenticate.sjs
   form_basic.html
+  formless_basic.html
   insecure_test.html
   insecure_test_subframe.html
   multiple_forms.html
   streamConverter_content.sjs
 
 [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 = os == "linux"
 [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,54 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const TEST_URL_PATH = "/browser/toolkit/components/passwordmgr/test/browser/";
+
+const WARNING_PATTERN = {
+  // "INSECURE_FORM_ACTION"
+  '[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."]': "INSECURE_FORM_ACTION",
+
+  // "INSECURE_PAGE"
+  '[JavaScript Warning: "Password fields present on an insecure (http://) page. This is a security risk that allows user login credentials to be stolen."]': "INSECURE_PAGE"
+};
+
+add_task(function* testInsecurePasswordWarning() {
+  for (let [origin, testPattern, expectWarnings] of [
+    ["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 + testPattern;
+    let tab = gBrowser.addTab(testUrlPath);
+    let browser = tab.linkedBrowser;
+    function listener(msg) {
+      // Only handle the insecure password related warning messages.
+      if (WARNING_PATTERN[msg.message]) {
+        var index = expectWarnings.indexOf(WARNING_PATTERN[msg.message]);
+
+        // Warning message found.
+        ok(index !== -1, "Warning message hits!");
+        if (index !== -1) {
+          // Remove the shown message.
+          expectWarnings[index] = null;
+        }
+      }
+    }
+    Services.console.registerListener(listener);
+
+    yield Promise.all([
+      BrowserTestUtils.switchTab(gBrowser, tab),
+      BrowserTestUtils.browserLoaded(browser),
+    ]);
+
+    // Verify if all warnings are shown.
+    for (let i = 0; i < expectWarnings.length; i++) {
+      ok(expectWarnings[i] === null, "All warnings are shown.");
+    }
+    Services.console.unregisterListener(listener);
+    gBrowser.removeTab(tab);
+  }
+});
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>