Bug 1302474 - Add a pref to disable login autofill on insecure forms. r=MattN
MozReview-Commit-ID: Fpz5108WvpR
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -61,22 +61,22 @@ addMessageListener("ContextMenu:DoCustom
});
addMessageListener("RemoteLogins:fillForm", function(message) {
LoginManagerContent.receiveMessage(message, content);
});
addEventListener("DOMFormHasPassword", function(event) {
LoginManagerContent.onDOMFormHasPassword(event, content);
let formLike = LoginFormFactory.createFromForm(event.target);
- InsecurePasswordUtils.checkForInsecurePasswords(formLike);
+ InsecurePasswordUtils.reportInsecurePasswords(formLike);
});
addEventListener("DOMInputPasswordAdded", function(event) {
LoginManagerContent.onDOMInputPasswordAdded(event, content);
let formLike = LoginFormFactory.createFromField(event.target);
- InsecurePasswordUtils.checkForInsecurePasswords(formLike);
+ InsecurePasswordUtils.reportInsecurePasswords(formLike);
});
addEventListener("pageshow", function(event) {
LoginManagerContent.onPageShow(event, content);
});
addEventListener("DOMAutoComplete", function(event) {
LoginManagerContent.onUsernameInput(event);
});
addEventListener("blur", function(event) {
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4315,16 +4315,17 @@ pref("font.name.monospace.x-unicode", "d
# AIX
#endif
// Login Manager prefs
pref("signon.rememberSignons", true);
pref("signon.rememberSignons.visibilityToggle", true);
pref("signon.autofillForms", true);
+pref("signon.autofillForms.http", true);
pref("signon.autologin.proxy", false);
pref("signon.formlessCapture.enabled", true);
pref("signon.storeWhenAutocompleteOff", true);
pref("signon.debug", false);
pref("signon.recipes.path", "chrome://passwordmgr/content/recipes.json");
pref("signon.schemeUpgrades", false);
// Satchel (Form Manager) prefs
--- a/toolkit/components/passwordmgr/InsecurePasswordUtils.jsm
+++ b/toolkit/components/passwordmgr/InsecurePasswordUtils.jsm
@@ -33,61 +33,88 @@ this.InsecurePasswordUtils = {
let message = bundle.GetStringFromName(messageTag);
let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
consoleMsg.initWithWindowID(message, domDoc.location.href, 0, 0, 0, flag, category, windowId);
Services.console.logMessage(consoleMsg);
},
/**
- * 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.
+ * Gets the security state of the passed form.
+ *
+ * @param {FormLike} aForm A form-like object. @See {FormLikeFactory}
*
- * @param {FormLike} aForm A form-like object. @See {LoginFormFactory}
+ * @returns {Object} An object with the following boolean values:
+ * isFormSubmitHTTP: if the submit action is an http:// URL
+ * isFormSubmitSecure: if the submit action URL is secure,
+ * either because it is HTTPS or because its origin is considered trustworthy
*/
- checkForInsecurePasswords(aForm) {
- if (this._formRootsWarned.has(aForm.rootElement) ||
- this._formRootsWarned.get(aForm.rootElement)) {
- return;
- }
-
- let domDoc = aForm.ownerDocument;
- let isSafePage = domDoc.defaultView.isSecureContext;
-
- if (!isSafePage) {
- if (domDoc.defaultView == domDoc.defaultView.parent) {
- this._sendWebConsoleMessage("InsecurePasswordsPresentOnPage", domDoc);
- } else {
- this._sendWebConsoleMessage("InsecurePasswordsPresentOnIframe", domDoc);
- }
- this._formRootsWarned.set(aForm.rootElement, true);
- }
-
+ _checkFormSecurity(aForm) {
let isFormSubmitHTTP = false, isFormSubmitSecure = false;
if (aForm.rootElement instanceof Ci.nsIDOMHTMLFormElement) {
let uri = Services.io.newURI(aForm.rootElement.action || aForm.rootElement.baseURI,
null, null);
let principal = gScriptSecurityManager.getCodebasePrincipal(uri);
if (uri.schemeIs("http")) {
isFormSubmitHTTP = true;
if (gContentSecurityManager.isOriginPotentiallyTrustworthy(principal)) {
isFormSubmitSecure = true;
- } else if (isSafePage) {
- // Only warn about the action if we didn't already warn about the form being insecure.
- this._sendWebConsoleMessage("InsecureFormActionPasswordsPresent", domDoc);
- this._formRootsWarned.set(aForm.rootElement, true);
}
} else {
isFormSubmitSecure = true;
}
}
+ return { isFormSubmitHTTP, isFormSubmitSecure };
+ },
+
+ /**
+ * 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.
+ *
+ * @param {FormLike} aForm A form-like object. @See {LoginFormFactory}
+ * @return {boolean} whether the form is secure
+ */
+ isFormSecure(aForm) {
+ let isSafePage = aForm.ownerDocument.defaultView.isSecureContext;
+ let { isFormSubmitSecure, isFormSubmitHTTP } = this._checkFormSecurity(aForm);
+
+ return isSafePage && (isFormSubmitSecure || !isFormSubmitHTTP);
+ },
+
+ /**
+ * Report insecure password fields in a form to the web console to warn developers.
+ *
+ * @param {FormLike} aForm A form-like object. @See {FormLikeFactory}
+ */
+ reportInsecurePasswords(aForm) {
+ if (this._formRootsWarned.has(aForm.rootElement) ||
+ this._formRootsWarned.get(aForm.rootElement)) {
+ return;
+ }
+
+ let domDoc = aForm.ownerDocument;
+ let isSafePage = domDoc.defaultView.isSecureContext;
+
+ let { isFormSubmitHTTP, isFormSubmitSecure } = this._checkFormSecurity(aForm);
+
+ if (!isSafePage) {
+ if (domDoc.defaultView == domDoc.defaultView.parent) {
+ this._sendWebConsoleMessage("InsecurePasswordsPresentOnPage", domDoc);
+ } else {
+ this._sendWebConsoleMessage("InsecurePasswordsPresentOnIframe", domDoc);
+ }
+ this._formRootsWarned.set(aForm.rootElement, true);
+ } else if (isFormSubmitHTTP && !isFormSubmitSecure) {
+ this._sendWebConsoleMessage("InsecureFormActionPasswordsPresent", domDoc);
+ this._formRootsWarned.set(aForm.rootElement, true);
+ }
+
// The safety of a password field determined by the form action and the page protocol
let passwordSafety;
if (isSafePage) {
if (isFormSubmitSecure) {
passwordSafety = 0;
} else if (isFormSubmitHTTP) {
passwordSafety = 1;
} else {
--- a/toolkit/components/passwordmgr/LoginHelper.jsm
+++ b/toolkit/components/passwordmgr/LoginHelper.jsm
@@ -32,16 +32,17 @@ Cu.import("resource://gre/modules/XPCOMU
*/
this.LoginHelper = {
/**
* Warning: these only update if a logger was created.
*/
debug: Services.prefs.getBoolPref("signon.debug"),
formlessCaptureEnabled: Services.prefs.getBoolPref("signon.formlessCapture.enabled"),
schemeUpgrades: Services.prefs.getBoolPref("signon.schemeUpgrades"),
+ insecureAutofill: Services.prefs.getBoolPref("signon.autofillForms.http"),
createLogger(aLogPrefix) {
let getMaxLogLevel = () => {
return this.debug ? "debug" : "warn";
};
// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
@@ -51,16 +52,17 @@ this.LoginHelper = {
};
let logger = new ConsoleAPI(consoleOptions);
// Watch for pref changes and update this.debug and the maxLogLevel for created loggers
Services.prefs.addObserver("signon.", () => {
this.debug = Services.prefs.getBoolPref("signon.debug");
this.formlessCaptureEnabled = Services.prefs.getBoolPref("signon.formlessCapture.enabled");
this.schemeUpgrades = Services.prefs.getBoolPref("signon.schemeUpgrades");
+ this.insecureAutofill = Services.prefs.getBoolPref("signon.autofillForms.http");
logger.maxLogLevel = getMaxLogLevel();
}, false);
return logger;
},
/**
* Due to the way the signons2.txt file is formatted, we need to make
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -9,16 +9,17 @@ this.EXPORTED_SYMBOLS = [ "LoginManagerC
"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");
+Cu.import("resource://gre/modules/InsecurePasswordUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FormLikeFactory",
"resource://gre/modules/FormLikeFactory.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoginRecipesContent",
"resource://gre/modules/LoginRecipes.jsm");
@@ -914,16 +915,17 @@ var LoginManagerContent = {
PASSWORD_DISABLED_READONLY: 2,
NO_LOGINS_FIT: 3,
NO_SAVED_LOGINS: 4,
EXISTING_PASSWORD: 5,
EXISTING_USERNAME: 6,
MULTIPLE_LOGINS: 7,
NO_AUTOFILL_FORMS: 8,
AUTOCOMPLETE_OFF: 9,
+ INSECURE: 10,
};
function recordAutofillResult(result) {
if (userTriggered) {
// Ignore fills as a result of user action.
return;
}
const autofillResultHist = Services.telemetry.getHistogramById("PWMGR_FORM_AUTOFILL_RESULT");
@@ -970,16 +972,24 @@ var LoginManagerContent = {
// If the password field is disabled or read-only, there's nothing to do.
if (passwordField.disabled || passwordField.readOnly) {
log("not filling form, password field disabled or read-only");
recordAutofillResult(AUTOFILL_RESULT.PASSWORD_DISABLED_READONLY);
return;
}
+ // Prevent autofilling insecure forms.
+ if (!userTriggered && !LoginHelper.insecureAutofill &&
+ !InsecurePasswordUtils.isFormSecure(form)) {
+ log("not filling form since it's insecure");
+ recordAutofillResult(AUTOFILL_RESULT.INSECURE);
+ return;
+ }
+
var isAutocompleteOff = false;
if (this._isAutocompleteDisabled(form) ||
this._isAutocompleteDisabled(usernameField) ||
this._isAutocompleteDisabled(passwordField)) {
isAutocompleteOff = true;
}
// Discard logins which have username/password values that don't
--- a/toolkit/components/passwordmgr/test/browser/browser.ini
+++ b/toolkit/components/passwordmgr/test/browser/browser.ini
@@ -36,16 +36,17 @@ support-files =
support-files =
subtst_notifications_change_p.html
[browser_DOMFormHasPassword.js]
[browser_DOMInputPasswordAdded.js]
[browser_exceptions_dialog.js]
[browser_formless_submit_chrome.js]
[browser_hasInsecureLoginForms.js]
[browser_hasInsecureLoginForms_streamConverter.js]
+[browser_http_autofill.js]
[browser_insecurePasswordWarning.js]
[browser_notifications.js]
[browser_notifications_username.js]
[browser_notifications_password.js]
[browser_notifications_2.js]
skip-if = os == "linux" # Bug 1272849 Main action button disabled state intermittent
[browser_passwordmgr_editing.js]
skip-if = os == "linux"
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_http_autofill.js
@@ -0,0 +1,78 @@
+const TEST_URL_PATH = "://example.org/browser/toolkit/components/passwordmgr/test/browser/";
+
+add_task(function* setup() {
+ let login = LoginTestUtils.testData.formLogin({
+ hostname: "http://example.org",
+ formSubmitURL: "http://example.org",
+ username: "username",
+ password: "password",
+ });
+ Services.logins.addLogin(login);
+ login = LoginTestUtils.testData.formLogin({
+ hostname: "http://example.org",
+ formSubmitURL: "http://another.domain",
+ username: "username",
+ password: "password",
+ });
+ Services.logins.addLogin(login);
+ yield SpecialPowers.pushPrefEnv({ "set": [["signon.autofillForms.http", false]] });
+});
+
+add_task(function* test_http_autofill() {
+ for (let scheme of ["http", "https"]) {
+ let tab = yield BrowserTestUtils
+ .openNewForegroundTab(gBrowser, `${scheme}${TEST_URL_PATH}form_basic.html`);
+
+ let {username, password} = yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ let doc = content.document;
+ let username = doc.getElementById("form-basic-username").value;
+ let password = doc.getElementById("form-basic-password").value;
+ return { username, password };
+ });
+
+ is(username, scheme == "http" ? "" : "username", "Username filled correctly");
+ is(password, scheme == "http" ? "" : "password", "Password filled correctly");
+
+ gBrowser.removeTab(tab);
+ }
+});
+
+add_task(function* test_iframe_in_http_autofill() {
+ for (let scheme of ["http", "https"]) {
+ let tab = yield BrowserTestUtils
+ .openNewForegroundTab(gBrowser, `${scheme}${TEST_URL_PATH}form_basic_iframe.html`);
+
+ let {username, password} = yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ let doc = content.document;
+ let iframe = doc.getElementById("test-iframe");
+ let username = iframe.contentWindow.document.getElementById("form-basic-username").value;
+ let password = iframe.contentWindow.document.getElementById("form-basic-password").value;
+ return { username, password };
+ });
+
+ is(username, scheme == "http" ? "" : "username", "Username filled correctly");
+ is(password, scheme == "http" ? "" : "password", "Password filled correctly");
+
+ gBrowser.removeTab(tab);
+ }
+});
+
+add_task(function* test_http_action_autofill() {
+ for (let type of ["insecure", "secure"]) {
+ let tab = yield BrowserTestUtils
+ .openNewForegroundTab(gBrowser, `https${TEST_URL_PATH}form_cross_origin_${type}_action.html`);
+
+ let {username, password} = yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ let doc = content.document;
+ let username = doc.getElementById("form-basic-username").value;
+ let password = doc.getElementById("form-basic-password").value;
+ return { username, password };
+ });
+
+ is(username, type == "insecure" ? "" : "username", "Username filled correctly");
+ is(password, type == "insecure" ? "" : "password", "Password filled correctly");
+
+ gBrowser.removeTab(tab);
+ }
+});
+