Bug 1193341 - Detect presence of password fields in any subframe, flagging those on insecure connections. r=MattN
--- a/toolkit/components/passwordmgr/InsecurePasswordUtils.jsm
+++ b/toolkit/components/passwordmgr/InsecurePasswordUtils.jsm
@@ -8,16 +8,18 @@ const Ci = Components.interfaces;
const Cu = Components.utils;
const Cc = Components.classes;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "devtools",
"resource://gre/modules/devtools/shared/Loader.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
+ "resource://gre/modules/LoginManagerContent.jsm");
Object.defineProperty(this, "WebConsoleUtils", {
get: function() {
return devtools.require("devtools/shared/webconsole/utils").Utils;
},
configurable: true,
enumerable: true
});
@@ -43,60 +45,16 @@ this.InsecurePasswordUtils = {
consoleMsg.initWithWindowID(
message, "", 0, 0, 0, flag, category, windowId);
Services.console.logMessage(consoleMsg);
},
/*
- * Checks whether the passed uri is secure
- * Check Protocol Flags to determine if scheme is secure:
- * URI_DOES_NOT_RETURN_DATA - e.g.
- * "mailto"
- * URI_IS_LOCAL_RESOURCE - e.g.
- * "data",
- * "resource",
- * "moz-icon"
- * URI_INHERITS_SECURITY_CONTEXT - e.g.
- * "javascript"
- * URI_SAFE_TO_LOAD_IN_SECURE_CONTEXT - e.g.
- * "https",
- * "moz-safe-about"
- *
- * The use of this logic comes directly from nsMixedContentBlocker.cpp
- * At the time it was decided to include these protocols since a secure
- * uri for mixed content blocker means that the resource can't be
- * easily tampered with because 1) it is sent over an encrypted channel or
- * 2) it is a local resource that never hits the network
- * or 3) it is a request sent without any response that could alter
- * the behavior of the page. It was decided to include the same logic
- * here both to be consistent with MCB and to make sure we cover all
- * "safe" protocols. Eventually, the code here and the code in MCB
- * will be moved to a common location that will be referenced from
- * both places. Look at
- * https://bugzilla.mozilla.org/show_bug.cgi?id=899099 for more info.
- */
- _checkIfURIisSecure : function(uri) {
- let isSafe = false;
- let netutil = Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil);
- let ph = Ci.nsIProtocolHandler;
-
- if (netutil.URIChainHasFlags(uri, ph.URI_IS_LOCAL_RESOURCE) ||
- netutil.URIChainHasFlags(uri, ph.URI_DOES_NOT_RETURN_DATA) ||
- netutil.URIChainHasFlags(uri, ph.URI_INHERITS_SECURITY_CONTEXT) ||
- netutil.URIChainHasFlags(uri, ph.URI_SAFE_TO_LOAD_IN_SECURE_CONTEXT)) {
-
- isSafe = true;
- }
-
- return isSafe;
- },
-
- /*
* Checks whether the passed nested document is insecure
* or is inside an insecure parent document.
*
* We check the chain of frame ancestors all the way until the top document
* because MITM attackers could replace https:// iframes if they are nested inside
* http:// documents with their own content, thus creating a security risk
* and potentially stealing user data. Under such scenario, a user might not
* get a Mixed Content Blocker message, if the main document is served over HTTP
@@ -104,17 +62,17 @@ this.InsecurePasswordUtils = {
* inside https).
*/
_checkForInsecureNestedDocuments : function(domDoc) {
let uri = domDoc.documentURIObject;
if (domDoc.defaultView == domDoc.defaultView.parent) {
// We are at the top, nothing to check here
return false;
}
- if (!this._checkIfURIisSecure(uri)) {
+ if (!LoginManagerContent.checkIfURIisSecure(uri)) {
// We are insecure
return true;
}
// I am secure, but check my parent
return this._checkForInsecureNestedDocuments(domDoc.defaultView.parent.document);
},
@@ -122,17 +80,17 @@ 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.
*/
checkForInsecurePasswords : function (aForm) {
var domDoc = aForm.ownerDocument;
let pageURI = domDoc.defaultView.top.document.documentURIObject;
- let isSafePage = this._checkIfURIisSecure(pageURI);
+ let isSafePage = LoginManagerContent.checkIfURIisSecure(pageURI);
if (!isSafePage) {
this._sendWebConsoleMessage("InsecurePasswordsPresentOnPage", domDoc);
}
// Check if we are on an iframe with insecure src, or inside another
// insecure iframe or document.
if (this._checkForInsecureNestedDocuments(domDoc)) {
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -409,25 +409,38 @@ var LoginManagerContent = {
let loginForm = getFirstLoginForm(frame);
if (loginForm) {
return loginForm;
}
}
return null;
};
+ // Returns true if this window or any subframes have insecure login forms.
+ let hasInsecureLoginForms = (thisWindow, parentIsInsecure) => {
+ let doc = thisWindow.document;
+ let isInsecure =
+ parentIsInsecure ||
+ !this.checkIfURIisSecure(doc.documentURIObject);
+ let hasLoginForm = !!this.stateForDocument(doc).loginForm;
+ return (hasLoginForm && isInsecure) ||
+ Array.some(thisWindow.frames,
+ frame => hasInsecureLoginForms(frame, isInsecure));
+ };
+
// Store the actual form to use on the state for the top-level document.
let topState = this.stateForDocument(topWindow.document);
topState.loginFormForFill = getFirstLoginForm(topWindow);
// Determine whether to show the anchor icon for the current tab.
let messageManager = messageManagerFromWindow(topWindow);
messageManager.sendAsyncMessage("RemoteLogins:updateLoginFormPresence", {
loginFormOrigin,
loginFormPresent: !!topState.loginFormForFill,
+ hasInsecureLoginForms: hasInsecureLoginForms(topWindow, false),
});
},
/**
* Perform a password fill upon user request coming from the parent process.
* The fill will be in the form previously identified during page navigation.
*
* @param An object with the following properties:
@@ -1084,16 +1097,59 @@ var LoginManagerContent = {
},
passwordField: {
found: !!newPasswordField,
disabled: newPasswordField && (newPasswordField.disabled || newPasswordField.readOnly),
},
};
},
+ /*
+ * Checks whether the passed uri is secure
+ * Check Protocol Flags to determine if scheme is secure:
+ * URI_DOES_NOT_RETURN_DATA - e.g.
+ * "mailto"
+ * URI_IS_LOCAL_RESOURCE - e.g.
+ * "data",
+ * "resource",
+ * "moz-icon"
+ * URI_INHERITS_SECURITY_CONTEXT - e.g.
+ * "javascript"
+ * URI_SAFE_TO_LOAD_IN_SECURE_CONTEXT - e.g.
+ * "https",
+ * "moz-safe-about"
+ *
+ * The use of this logic comes directly from nsMixedContentBlocker.cpp
+ * At the time it was decided to include these protocols since a secure
+ * uri for mixed content blocker means that the resource can't be
+ * easily tampered with because 1) it is sent over an encrypted channel or
+ * 2) it is a local resource that never hits the network
+ * or 3) it is a request sent without any response that could alter
+ * the behavior of the page. It was decided to include the same logic
+ * here both to be consistent with MCB and to make sure we cover all
+ * "safe" protocols. Eventually, the code here and the code in MCB
+ * will be moved to a common location that will be referenced from
+ * both places. Look at
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=899099 for more info.
+ */
+ checkIfURIisSecure : function(uri) {
+ let isSafe = false;
+ let netutil = Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil);
+ let ph = Ci.nsIProtocolHandler;
+
+ if (netutil.URIChainHasFlags(uri, ph.URI_IS_LOCAL_RESOURCE) ||
+ netutil.URIChainHasFlags(uri, ph.URI_DOES_NOT_RETURN_DATA) ||
+ netutil.URIChainHasFlags(uri, ph.URI_INHERITS_SECURITY_CONTEXT) ||
+ netutil.URIChainHasFlags(uri, ph.URI_SAFE_TO_LOAD_IN_SECURE_CONTEXT)) {
+
+ isSafe = true;
+ }
+
+ return isSafe;
+ },
};
var LoginUtils = {
/*
* _getPasswordOrigin
*
* Get the parts of the URL we want for identification.
*/
--- a/toolkit/components/passwordmgr/LoginManagerParent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm
@@ -565,31 +565,47 @@ var LoginManagerParent = {
if (!loginFormState) {
loginFormState = {};
this.loginFormStateByBrowser.set(browser, loginFormState);
}
return loginFormState;
},
/**
+ * Returns true if the page currently loaded in the given browser element has
+ * insecure login forms. This state may be updated asynchronously, in which
+ * case a custom event named InsecureLoginFormsStateChange will be dispatched
+ * on the browser element.
+ */
+ hasInsecureLoginForms(browser) {
+ return !!this.stateForBrowser(browser).hasInsecureLoginForms;
+ },
+
+ /**
* Called to indicate whether a login form on the currently loaded page is
* present or not. This is one of the factors used to control the visibility
* of the password fill doorhanger.
*/
- updateLoginFormPresence(browser, { loginFormOrigin, loginFormPresent }) {
+ updateLoginFormPresence(browser, { loginFormOrigin, loginFormPresent,
+ hasInsecureLoginForms }) {
const ANCHOR_DELAY_MS = 200;
let state = this.stateForBrowser(browser);
// Update the data to use to the latest known values. Since messages are
// processed in order, this will always be the latest version to use.
state.loginFormOrigin = loginFormOrigin;
state.loginFormPresent = loginFormPresent;
+ state.hasInsecureLoginForms = hasInsecureLoginForms;
- // Apply the data to the currently displayed icon later.
+ // Report the insecure login form state immediately.
+ browser.dispatchEvent(new browser.ownerDocument.defaultView
+ .CustomEvent("InsecureLoginFormsStateChange"));
+
+ // Apply the data to the currently displayed login fill icon later.
if (!state.anchorDeferredTask) {
state.anchorDeferredTask = new DeferredTask(
() => this.updateLoginAnchor(browser),
ANCHOR_DELAY_MS
);
}
state.anchorDeferredTask.arm();
},
--- a/toolkit/components/passwordmgr/test/browser/browser.ini
+++ b/toolkit/components/passwordmgr/test/browser/browser.ini
@@ -1,17 +1,20 @@
[DEFAULT]
support-files =
authenticate.sjs
form_basic.html
+ insecure_test.html
+ insecure_test_subframe.html
multiple_forms.html
[browser_DOMFormHasPassword.js]
[browser_DOMInputPasswordAdded.js]
[browser_filldoorhanger.js]
+[browser_hasInsecureLoginForms.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_hasInsecureLoginForms.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/LoginManagerParent.jsm", this);
+
+const testUrlPath =
+ "://example.com/browser/toolkit/components/passwordmgr/test/browser/";
+
+/**
+ * Waits for the given number of occurrences of InsecureLoginFormsStateChange
+ * on the given browser element.
+ */
+function waitForInsecureLoginFormsStateChange(browser, count) {
+ return BrowserTestUtils.waitForEvent(browser, "InsecureLoginFormsStateChange",
+ false, () => --count == 0);
+}
+
+/**
+ * Checks that hasInsecureLoginForms is true for a simple HTTP page and false
+ * for a simple HTTPS page.
+ */
+add_task(function* test_simple() {
+ for (let scheme of ["http", "https"]) {
+ let tab = gBrowser.addTab(scheme + testUrlPath + "form_basic.html");
+ let browser = tab.linkedBrowser;
+ yield Promise.all([
+ BrowserTestUtils.switchTab(gBrowser, tab),
+ BrowserTestUtils.browserLoaded(browser),
+ // One event is triggered by pageshow and one by DOMFormHasPassword.
+ waitForInsecureLoginFormsStateChange(browser, 2),
+ ]);
+
+ Assert.equal(LoginManagerParent.hasInsecureLoginForms(browser),
+ scheme == "http");
+
+ gBrowser.removeTab(tab);
+ }
+});
+
+/**
+ * Checks that hasInsecureLoginForms is true if a password field is present in
+ * an HTTP page loaded as a subframe of a top-level HTTPS page, when mixed
+ * active content blocking is disabled.
+ *
+ * When the subframe is navigated to an HTTPS page, hasInsecureLoginForms should
+ * be set to false.
+ *
+ * Moving back in history should set hasInsecureLoginForms to true again.
+ */
+add_task(function* test_subframe_navigation() {
+ yield new Promise(resolve => SpecialPowers.pushPrefEnv({
+ "set": [["security.mixed_content.block_active_content", false]],
+ }, resolve));
+
+ // Load the page with the subframe in a new tab.
+ let tab = gBrowser.addTab("https" + testUrlPath + "insecure_test.html");
+ let browser = tab.linkedBrowser;
+ yield Promise.all([
+ BrowserTestUtils.switchTab(gBrowser, tab),
+ BrowserTestUtils.browserLoaded(browser),
+ // Two events are triggered by pageshow and one by DOMFormHasPassword.
+ waitForInsecureLoginFormsStateChange(browser, 3),
+ ]);
+
+ Assert.ok(LoginManagerParent.hasInsecureLoginForms(browser));
+
+ // Navigate the subframe to a secure page.
+ let promiseSubframeReady = Promise.all([
+ BrowserTestUtils.browserLoaded(browser, true),
+ // One event is triggered by pageshow and one by DOMFormHasPassword.
+ waitForInsecureLoginFormsStateChange(browser, 2),
+ ]);
+ yield ContentTask.spawn(browser, null, function* () {
+ content.document.getElementById("test-iframe")
+ .contentDocument.getElementById("test-link").click();
+ });
+ yield promiseSubframeReady;
+
+ Assert.ok(!LoginManagerParent.hasInsecureLoginForms(browser));
+
+ // Navigate back to the insecure page. We only have to wait for the
+ // InsecureLoginFormsStateChange event that is triggered by pageshow.
+ let promise = waitForInsecureLoginFormsStateChange(browser, 1);
+ yield ContentTask.spawn(browser, null, function* () {
+ content.document.getElementById("test-iframe")
+ .contentWindow.history.back();
+ });
+ yield promise;
+
+ Assert.ok(LoginManagerParent.hasInsecureLoginForms(browser));
+
+ gBrowser.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/insecure_test.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- This frame is initially loaded over HTTP. -->
+<iframe id="test-iframe"
+ src="http://example.org/browser/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html"/>
+
+</body></html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<form>
+ <input name="password" type="password">
+</form>
+
+<!-- Link to reload this page over HTTPS. -->
+<a id="test-link"
+ href="https://example.org/browser/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html">HTTPS</a>
+
+</body></html>