PIN Bypass in Passwordless WebAuthn on microsoft.com and Nextcloud
While implementing FIDO2 and WebAuthn support in our Hardware Security SDK, we found a way to bypass the PIN when logging into microsoft.com.
What is FIDO2/WebAuthn?
FIDO2/WebAuthn is an open standard, supported by browsers and driven by tech companies, such as Google and Microsoft. Many websites already adopted the FIDO standard for two-factor authentication. This means, in addition to a password, a login requires an additional FIDO hardware device, such as a YubiKey.
FIDO2/WebAuthn goes one step further and allows users to login without passwords, creating passwordless authentication. The standard is built on Public Key Cryptography and thus eliminates the need to exchange a shared secret between user and website.
With our experience in cryptographic protocols, we evaluated the WebAuthn specification and its threat model. The technology is well thought through, focused on the scenario of authentication and makes the right trade-offs. We have mostly praise for its design.
WebAuthn Passwordless Authentication
Passwordless authentication is yet to be adopted widely. A prominent example that already implements this is Microsoft with its Azure Active Directory.
First, the user adds a security key to her account. The browser will ask the user to set a PIN on her security key. The registration is completed by touching the button on the security key.
Now, on each login, the user needs the PIN and her security key (multi-factor authentication: “something you know and something you have”).
PIN Bypass in Passwordless Authentication
We were able to bypass the PIN when logging into microsoft.com. This breakes the assumption of requiring two factors and allows an attacker to log into the victim’s account by using the security key only. He could steal the victim’s USB security key and login without a PIN. Attacking the victim over NFC is even easier by sneaking up on the victim without getting noticed.
Technical Details
On a FIDO2 security key, there is only one PIN, not multiple PINs. Just because a PIN has been set does not mean that all FIDO2 credentials will require a PIN from that point on.
To allow getAssertion
calls with or without PIN, the website decides this individually per operation.
This is done by setting the optional
userVerification
property to either required
, preferred
, or discouraged
.
Microsoft’s website did not provide a value (see
Javascript Snippet in Appendix).
In this case, Chrome and other browsers will either ask the user to set a PIN or ask the user to enter the existing PIN (see
Chrome’s notice regarding preferred
).
It is important to note that userVerification
only influences how the browser communicates with the security key.
Setting userVerification = "required"
provides no cryptographic guarantee by itself that the getAssertion
call will always require a PIN.
Instead it is up to the Relying Party (the WebAuthn server) to verify that the cryptographic signature in authData
has the UV flag set (Step 17 in
7.2. Verifying an Authentication Assertion).
This is also noted briefly in Yuriy Ackermann’s blog post (Step 6 under the section
“Verifying response”).
So to fix this issue, on Microsoft’s implementation of the Relying Party, the UV flag in authData
must be checked.
Microsoft’s Response
We reported the issue to Microsoft. They did not consider it a vulnerability, but fixed it:
Thank you for reporting this issue to Microsoft. Our team investigated this, and we agree that userVerification should be required in the described scenarios as part of a defense in depth strategy. We are grateful to the finder for their work in identifying this issue and reporting it to Microsoft.
It is important to note that when a UserPresence check was used in place of a UserVerification check, the resulting token issued reflected correctly that a single-factor authentication occurred. This token would not be usable to access resources protected by strong-auth (MFA) policies or elevate access. This does not lessen the significance of the research but does mitigate the impact of the finding.
We verified that Microsoft indeed fixed the PIN bypass issue.
We do not agree with Microsoft that this was not a vulnerability. If a PIN is requested by the browser during login, users expect that this PIN is actually verified. This was not the case and lead to a false sense of security. Even for us security developers, this was highly unexpected.
Nextcloud 19
Nextcloud introduced WebAuthn passwordless authentication with version 19. In an announcement of Nextcloud’s cooperation with Nitrokey, it is written that this feature provides two-factor authentication:
The server asking for authentication can request verification of multiple factors, so that a configured key requires the user to not just plug it in but also enter a PIN or scan a finger print.
We found that in Nextcloud 19.0.0 and 19.0.1, userVerification
is not set and the UV flag is not checked on the server.
Thus, even though a FIDO2 key with a PIN is added in a user account, the PIN is not required to log in.
Nextcloud’s Response
We discussed the issue with the Nextcloud team privately.
They do not consider this a vulnerability as the feature is supposedly not designed to provide two-factor authentication but only single-factor authentication.
They agree that Nitrokey’s blog post is incorrect (it has now been updated) and they
implemented userVerification = "discouraged"
for their next maintenance release, which will stop browsers from asking for a PIN.
For two-factor authentication, they recommend users to enable TOTP or Nextcloud Notifications.
Like with Microsoft’s response, we think that if the user is asked to provide a PIN, the user expects that the PIN is verified. We recommend that Nextcloud documents their threat model and communicates that their passwordless authentication does not provide two-factor protection.
Inconsistencies between Spec and Implementation
WebAuthn specifies that the
UV flag should not be checked if the website sets userVerification = 'preferred'
.
This is unexpected to web developers who observe that Chrome requires a PIN and conclude that their code provides two-factor protection.
In fact it only does so when checking the UV flag on the server.
We recommend that the FIDO Alliance re-think their position on this matter (see
related GitHub discussions) to better match the developers expectations.
Reproducing the Issue
If you like to try how different websites implement WebAuthn, try our SDK example app on Google Play. We added a button to skip the PIN authentication.
We also provide a
Tampermonkey script that can be installed in Chrome to override userVerification
to always be discouraged
.
What about other passwordless WebAuthn logins?
This issue is also present on Yubico’s Playground. Due to time constraints we haven’t yet evaluated other implementations of a FIDO2 Relying Party. A good overview of existing projects can be found on Yuriy Ackermann’s Github repo.
Timeline
- 2020-10-28: Nextcloud assigned CVE-2020-8236 and NC-SA-2020-037
- 2020-08-12: We published this blog post
- 2020-07-20: Added Nextcloud’s response
- 2020-07-15: Nextcloud 19 tested, included in post and reported to Nextcloud
- 2020-07-09: Microsoft fixed the issue but does not consider it a vulnerability
- 2020-06-22: Added more context about userVerification = ‘preferred’
- 2020-06-18: Finished blog post, post is not listed publicly
- 2020-06-18: Reported to Microsoft as VULN-026995
Appendix
In our tests, the useNewDefaults
branch is not executed, thus userVerification
is not set.
exports.getAssertion = function (serverChallenge, serverAllowList, rpId)
{
var allowListParam = [];
if (serverAllowList)
{
allowListParam = serverAllowList.map(
function (credentialId)
{
return { type: "public-key",
id: TypeConverter.base64UrlStringToArrayBuffer(credentialId) };
});
}
var publicKeyCredentialRequestOptions =
{
challenge: TypeConverter.stringToArrayBuffer(serverChallenge),
timeout: FidoConstants.Timeout,
rpId: rpId,
allowCredentials: allowListParam
};
if (useNewDefaults)
{
publicKeyCredentialRequestOptions.userVerification = "required";
}
return n.credentials.get({ publicKey: publicKeyCredentialRequestOptions });
};
- Install Tampermonkey
- Add this script:
// ==UserScript==
// @name Skip WebAuthn PIN verification
// @namespace https://hwsecurity.dev
// @version 1.0
// @description Discourage WebAuthn userVerification
// @author Vincent Breitmoser
// @match https://*/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const origGet = navigator.credentials.get.bind(navigator.credentials);
navigator.credentials.get = function(arg) { console.log(arg); arg.publicKey.userVerification = "discouraged"; return origGet(arg); }
})();