Bug 1302474 - Add a pref to disable login autofill on insecure forms. r=MattN draft
authorJohann Hofmann <jhofmann@mozilla.com>
Tue, 13 Sep 2016 12:04:46 +0200
changeset 429414 62b5ad3289dd7ac229bd7270cdc9717ac5695a2c
parent 429408 b1b18f25c0ea69d9ee57c4198d577dfcd0129ce1
child 534967 25834343e2fbe8f95021a74ce0b67cf16c5e4510
push id33565
push userbmo:jhofmann@mozilla.com
push dateTue, 25 Oct 2016 21:02:10 +0000
reviewersMattN
bugs1302474
milestone52.0a1
Bug 1302474 - Add a pref to disable login autofill on insecure forms. r=MattN MozReview-Commit-ID: Fpz5108WvpR
browser/base/content/content.js
modules/libpref/init/all.js
toolkit/components/passwordmgr/InsecurePasswordUtils.jsm
toolkit/components/passwordmgr/LoginHelper.jsm
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/test/browser/browser.ini
toolkit/components/passwordmgr/test/browser/browser_http_autofill.js
--- 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);
+  }
+});
+