Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hash the login nonce for storage to avoid leaking it via DB access #453

Merged
merged 17 commits into from
Sep 8, 2022

Conversation

kasparsd
Copy link
Collaborator

@kasparsd kasparsd commented Sep 8, 2022

Hash the second factor login screen nonce in storage to avoid leaking it via DB access but pass in unhashed to the second factor input form.

  • Add tests to verify the nonce checks.
  • Ensure every failed nonce check requires a fresh nonce.
  • Updating dev deps to the latest versions.

if ( $nonce !== $login_nonce['key'] || time() > $login_nonce['expiration'] ) {
self::delete_login_nonce( $user_id );
return false;
if ( hash_equals( self::hash_login_nonce( $nonce ), $login_nonce['key'] ) && time() < $login_nonce['expiration'] ) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a positive check for readability.

if ( ! update_user_meta( $user_id, self::USER_META_NONCE_KEY, $login_nonce ) ) {
// Store the nonce hashed to avoid leaking it via database access.
$login_nonce_stored = $login_nonce;
$login_nonce_stored['key'] = self::hash_login_nonce( $login_nonce['key'] );
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we hash the nonce for storage and pass the original to the form.

$login_nonce_stored = $login_nonce;
$login_nonce_stored['key'] = self::hash_login_nonce( $login_nonce['key'] );

if ( ! update_user_meta( $user_id, self::USER_META_NONCE_KEY, $login_nonce_stored ) ) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Store the nonce with the key hashed.

if ( ! $login_nonce ) {

// Require a fresh nonce for each request.
self::delete_login_nonce( $user_id );
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to not require a new nonce for every login attempt?

@@ -846,8 +866,6 @@ public static function login_form_validate_2fa() {
exit;
}

self::delete_login_nonce( $user->ID );
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nonces are now deleted whenever they are validated.

@@ -737,15 +748,21 @@ public static function login_url( $params = array(), $scheme = 'login' ) {
* @return array
*/
public static function create_login_nonce( $user_id ) {
$login_nonce = array();
$login_nonce = array(
'expiration' => time() + HOUR_IN_SECONDS,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need a nonce that is valid for an hour? How about we reduce it to 10 minutes or something like that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #473 👍🏻

@@ -775,16 +792,19 @@ public static function delete_login_nonce( $user_id ) {
*/
public static function verify_login_nonce( $user_id, $nonce ) {
$login_nonce = get_user_meta( $user_id, self::USER_META_NONCE_KEY, true );
if ( ! $login_nonce ) {

if ( ! $login_nonce || empty( $login_nonce['key'] ) || empty( $login_nonce['expiration'] ) ) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Account for missing nonce params in the DB. This should never happen in normal circumstance.

}
},
"extra": {
"wordpress-install-dir": "wordpress"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was needed after updating to roots/wordpress-full which has the core themes bundled.

@jeffpaul jeffpaul added this to the 0.8.0 milestone Sep 8, 2022
@georgestephanis
Copy link
Collaborator

Looks reasonable. Shouldn't need any migration tooling either, as login nonces don't last, anyone in the middle of a login over the upgrade can just log in again, nbd.

@kasparsd kasparsd merged commit f3232d9 into master Sep 8, 2022
@kasparsd kasparsd deleted the add/totp-hashes branch September 8, 2022 13:56
@kasparsd kasparsd mentioned this pull request Sep 11, 2022
@calvinalkan
Copy link
Contributor

@jeffpaul @kasparsd

This looks good at first glance

Although I would like the $user_id and $expiration to be part of the MAC to absolutely guarantee that a given "challenge_token" can only ever be used for the user that created it and that the expiration can't be tampered with either.

$data = [
   'key' => bin2hex(random_bytes(32)), 
   'user_id' => $user->ID,
   'expiration' => $expiration
]

$hashed_validator = hash_hmac('sha256', json_encode($data, JSON_THROW_ON_ERROR), wp_salt('nonce'));

$store_me = [
 'expiration => $expiration, 
 'hashed_validator => $hashed_validator
]

On the validation side you just reconstruct the validator from the single parts

@jeffpaul
Copy link
Member

@calvinalkan we're working on another release in the (hopefully) near-term, so if you are able to push up a PR for your suggestion then I can coordinate to get review/merge on that... thanks!

@iandunn iandunn mentioned this pull request Oct 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants