Bug 1191092 - Check password box not inside a form element.; r?MattN
MozReview-Commit-ID: Q5abQmgdhA
--- 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>