Bug 1193341 - Detect presence of password fields in any subframe, flagging those on insecure connections. r=MattN

--HG--
extra : commitid : ADlptzF6ATB
extra : rebase_source : dc352aa8cd85aec40d2fefbf0536d57c25ce316e
This commit is contained in:
Paolo Amadini 2015-10-13 13:40:34 +01:00
Родитель 5a213c7dfc
Коммит 2654050231
7 изменённых файлов: 196 добавлений и 48 удалений

Просмотреть файл

@ -13,6 +13,8 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "devtools", XPCOMUtils.defineLazyModuleGetter(this, "devtools",
"resource://gre/modules/devtools/shared/Loader.jsm"); "resource://gre/modules/devtools/shared/Loader.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
"resource://gre/modules/LoginManagerContent.jsm");
Object.defineProperty(this, "WebConsoleUtils", { Object.defineProperty(this, "WebConsoleUtils", {
get: function() { get: function() {
@ -47,50 +49,6 @@ this.InsecurePasswordUtils = {
Services.console.logMessage(consoleMsg); 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 * Checks whether the passed nested document is insecure
* or is inside an insecure parent document. * or is inside an insecure parent document.
@ -109,7 +67,7 @@ this.InsecurePasswordUtils = {
// We are at the top, nothing to check here // We are at the top, nothing to check here
return false; return false;
} }
if (!this._checkIfURIisSecure(uri)) { if (!LoginManagerContent.checkIfURIisSecure(uri)) {
// We are insecure // We are insecure
return true; return true;
} }
@ -127,7 +85,7 @@ this.InsecurePasswordUtils = {
checkForInsecurePasswords : function (aForm) { checkForInsecurePasswords : function (aForm) {
var domDoc = aForm.ownerDocument; var domDoc = aForm.ownerDocument;
let pageURI = domDoc.defaultView.top.document.documentURIObject; let pageURI = domDoc.defaultView.top.document.documentURIObject;
let isSafePage = this._checkIfURIisSecure(pageURI); let isSafePage = LoginManagerContent.checkIfURIisSecure(pageURI);
if (!isSafePage) { if (!isSafePage) {
this._sendWebConsoleMessage("InsecurePasswordsPresentOnPage", domDoc); this._sendWebConsoleMessage("InsecurePasswordsPresentOnPage", domDoc);

Просмотреть файл

@ -414,6 +414,18 @@ var LoginManagerContent = {
return null; 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. // Store the actual form to use on the state for the top-level document.
let topState = this.stateForDocument(topWindow.document); let topState = this.stateForDocument(topWindow.document);
topState.loginFormForFill = getFirstLoginForm(topWindow); topState.loginFormForFill = getFirstLoginForm(topWindow);
@ -423,6 +435,7 @@ var LoginManagerContent = {
messageManager.sendAsyncMessage("RemoteLogins:updateLoginFormPresence", { messageManager.sendAsyncMessage("RemoteLogins:updateLoginFormPresence", {
loginFormOrigin, loginFormOrigin,
loginFormPresent: !!topState.loginFormForFill, loginFormPresent: !!topState.loginFormForFill,
hasInsecureLoginForms: hasInsecureLoginForms(topWindow, false),
}); });
}, },
@ -1089,6 +1102,49 @@ var LoginManagerContent = {
}; };
}, },
/*
* 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 = { var LoginUtils = {

Просмотреть файл

@ -569,12 +569,23 @@ var LoginManagerParent = {
return 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 * 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 * present or not. This is one of the factors used to control the visibility
* of the password fill doorhanger. * of the password fill doorhanger.
*/ */
updateLoginFormPresence(browser, { loginFormOrigin, loginFormPresent }) { updateLoginFormPresence(browser, { loginFormOrigin, loginFormPresent,
hasInsecureLoginForms }) {
const ANCHOR_DELAY_MS = 200; const ANCHOR_DELAY_MS = 200;
let state = this.stateForBrowser(browser); let state = this.stateForBrowser(browser);
@ -583,8 +594,13 @@ var LoginManagerParent = {
// processed in order, this will always be the latest version to use. // processed in order, this will always be the latest version to use.
state.loginFormOrigin = loginFormOrigin; state.loginFormOrigin = loginFormOrigin;
state.loginFormPresent = loginFormPresent; 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) { if (!state.anchorDeferredTask) {
state.anchorDeferredTask = new DeferredTask( state.anchorDeferredTask = new DeferredTask(
() => this.updateLoginAnchor(browser), () => this.updateLoginAnchor(browser),

Просмотреть файл

@ -2,11 +2,14 @@
support-files = support-files =
authenticate.sjs authenticate.sjs
form_basic.html form_basic.html
insecure_test.html
insecure_test_subframe.html
multiple_forms.html multiple_forms.html
[browser_DOMFormHasPassword.js] [browser_DOMFormHasPassword.js]
[browser_DOMInputPasswordAdded.js] [browser_DOMInputPasswordAdded.js]
[browser_filldoorhanger.js] [browser_filldoorhanger.js]
[browser_hasInsecureLoginForms.js]
[browser_notifications.js] [browser_notifications.js]
skip-if = true # Intermittent failures: Bug 1182296, bug 1148771 skip-if = true # Intermittent failures: Bug 1182296, bug 1148771
[browser_passwordmgr_editing.js] [browser_passwordmgr_editing.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);
});

Просмотреть файл

@ -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>

Просмотреть файл

@ -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>