w3c / webappsec-credential-management

WebAppSec Credential Management
https://w3c.github.io/webappsec-credential-management/
Other
49 stars 38 forks source link

Require call to navigator.credentials.store before sharing credentials across subdomains #102

Open lawnsea opened 7 years ago

lawnsea commented 7 years ago

Under the current implementation of Credential Management in Chrome, credentials that the user stores on a subdomain via the browser's Google Smart Lock UI are exposed programmatically to scripts running in pages on other subdomains of the site. I found this behavior surprising - in part because the blog post announcing the feature said that credentials had to be explicitly stored via navigator.credentials.store to be available to another subdomain via navigator.credentials.get. I filed a Chrome security bug that was closed WontFix.

I think Chrome's implementation leans too far toward user convenience at the cost of user safety. Specifically, it significantly lowers the barrier for malicious scripts to phish credentials. Before, the script had to construct a login form that appears legitimate to the user and induce them to select their credentials to autofill (or type them in). In many cases, the form must be customized for each site. Now, the script can simply call navigator.credentials.get and the browser will display a UI that the user trusts. This makes it feasible, for example, to create a malicious ad that tries to grab credentials from any site it is displayed on.

I suggest that the spec require a call to navigator.credentials.store before making a credential available via navigator.credentials.get on another subdomain.

Reproduction steps

1) Using Chrome 57+, navigate to https://admin-dot-example.glitch.me/ 2) Enter a username and password and click 'Log in' 3) Click 'Save' to indicate you want Google Smart Lock to save your password for this site 4) Click 'Click here to repro' link 5) Click 'Sign In' to indicate you want to sign in with your account saved with Google Smart Lock 6) Observe that your username and password are displayed

lawnsea commented 7 years ago

As a workaround, the site can "opt out" of the Credential Management API by clobbering navigator.credentials:

if (navigator.credentials) {
  Object.defineProperty(navigator, 'credentials', {
    configurable: false,
    enumberable: true,
    value: {
      get: () => Promise.reject('Credentials API disabled')
    },
    writable: false
  });
}

This depends on how the user agent treats host objects, however, so it may not be a reliable mitigation across browsers.

battre commented 7 years ago

When I follow your steps to produce, I get the following output:

This page will prompt the user to log in and then output their login credentials below.

Exploit failed.

The expected (and hopefully implemented) behavior in Chrome is the following:

  1. Store a credentials on a.example.com (does not matter whether via CM API or by a username/password form.)
  2. Navigate to b.example.com
  3. b.example.com can call navigator.credentials.get(..., mediation: silent) --> it won't get anything. b.example.com can call navigator.credentials.get(..., mediation: optional) --> a UI pops up to ask for confirmation to give b.example.com the credentials.
  4. Assume that the user clicked on the credential, restarted the browser, the session expired and the user navigates to b.example.com again. The user would observe the same behavior as in step 3. Just having clicked on the credential does not change any internal state.
  5. Now, assume that the user picks a credential and b.example.com calls navigator.credentials.store(). For subsequent visits, navigator.credentials.get(..., mediation: silent) will return the credential to b.example.com also silently because the credential has been stored for that specific security origin.

I suggest to address this issue as follows

  1. Do you observe that described behavior as well, when you clear persisted credentials from your experiment?
  2. Do you object to this behavior?
  3. If the answer to 2. is yes or no, do we need to change the spec? Do you think we should prescribe a behavior or is it up to the browser vendor to pick one that is in line with their general behavior around password management?
lawnsea commented 7 years ago

When I follow your steps to produce, I get the following output:

Exploit failed.

Not sure why that is. It works for me and others who have tried it. Did you try it in an incognito window? If so, that won't work if the credentials weren't previously already saved in a normal window.

Do you observe that described behavior as well, when you clear persisted credentials from your experiment?

Yes. My repro demonstrates all but step 5.

Do you object to this behavior?

I do. Specifically, this part:

b.example.com can call navigator.credentials.get(..., mediation: optional) --> a UI pops up to ask for confirmation to give b.example.com the credentials

In my opinion this makes it substantially easier for an attacker to phish credentials:

  1. The attacker doesn't have to render a login form that the user finds trustworthy - the browser does it for them
  2. The UX doesn't give any indication signing in will result in their plaintext credentials being given to the site
  3. The UX trains users to just click 'Sign In'

An attacker could, for example, craft a malicious ad that attempts to grab credentials on any site where it is displayed. The ad doesn't need to know anything about the site, because it can co-opt the browser to provide the illusion of legitimacy.

If the answer to 2. is yes or no, do we need to change the spec? Do you think we should prescribe a behavior or is it up to the browser vendor to pick one that is in line with their general behavior around password management?

I think the spec should be changed to require that browsers not allow credentials.get of credentials not stored by calling credentials.store.

Chrome's implementation opts all websites in. I would prefer that sites opt themselves in explicitly.

battre commented 7 years ago

Thanks for your response. I understand your concerns now. The problem is not that credentials are passed silently (they are not) but that the user sees a confirmation dialog.

The general use case of sharing credentials across subdomains is important because we observe many cases of m.example.com and www.example.com for mobile/desktop respectively.

We could make sharing credentials across subdomains an opt-in or opt-out feature but I am not sure whether it really moves the needle. Unless I miss anything, to make a difference the following conditions need to be met:

The case where this does make a difference is if you have two subdomains with completely different login systems (separate login databases). I think this is a rare but existing case. For the CM API and the form based password manager in Chrome we chose to default to sharing credentials to subdomains that are matched by Mozilla's PSL list (https://publicsuffix.org/) and we are considering to add an opt out for users for specific domains.

@mikewest do you have an opinion w.r.t. supporting this and making it opt-in or opt-out?