Application Passwords: Correct the fallback behaviour for application passwords that don't use a generic hash.

Application passwords that aren't hashed using BLAKE2b should be checked using wp_check_password() rather than assuming they were hashed with phpass. This provides full back compat support for application passwords that were created via an overridden wp_hash_password() function that uses an alternative hashing algorithm.

Reviewed by audrasjb.
Merges [60123] into the 6.8 branch.

Props snicco, debarghyabanerjee, peterwilsoncc, jorbin, johnbillion.

Fixes 


git-svn-id: https://develop.svn.wordpress.org/branches/6.8@60125 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
John Blackbourn 2025-04-03 14:36:46 +00:00
parent 295bfbad4e
commit 0c56b81904
3 changed files with 98 additions and 3 deletions

@ -502,6 +502,14 @@ class WP_Application_Passwords {
string $password,
string $hash
): bool {
if ( ! str_starts_with( $hash, '$generic$' ) ) {
/*
* If the hash doesn't start with `$generic$`, it is a hash created with `wp_hash_password()`.
* This is the case for application passwords created before 6.8.0.
*/
return wp_check_password( $password, $hash );
}
return wp_verify_fast_hash( $password, $hash );
}
}

@ -9150,8 +9150,8 @@ function wp_fast_hash(
* Checks whether a plaintext message matches the hashed value. Used to verify values hashed via wp_fast_hash().
*
* The function uses Sodium to hash the message and compare it to the hashed value. If the hash is not a generic hash,
* the hash is treated as a phpass portable hash in order to provide backward compatibility for application passwords
* which were hashed using phpass prior to WordPress 6.8.0.
* the hash is treated as a phpass portable hash in order to provide backward compatibility for passwords and security
* keys which were hashed using phpass prior to WordPress 6.8.0.
*
* @since 6.8.0
*

@ -1134,6 +1134,29 @@ class Tests_Auth extends WP_UnitTestCase {
$this->assertSame( self::$user_id, $user->ID );
}
/**
* @ticket 21022
* @ticket 63203
*/
public function test_plain_bcrypt_application_password_is_accepted() {
add_filter( 'application_password_is_api_request', '__return_true' );
add_filter( 'wp_is_application_passwords_available', '__return_true' );
$password = 'password';
// Set an application password with plain bcrypt, which mimics a password that was hashed with
// a custom `wp_hash_password()` in use.
$uuid = self::set_application_password_with_plain_bcrypt( $password, self::$user_id );
// Authenticate.
$user = wp_authenticate_application_password( null, self::USER_LOGIN, $password );
// Verify that the plain bcrypt hash for the application password was valid.
$this->assertNotWPError( $user );
$this->assertInstanceOf( 'WP_User', $user );
$this->assertSame( self::$user_id, $user->ID );
}
/**
* @dataProvider data_usernames
*
@ -1591,6 +1614,19 @@ class Tests_Auth extends WP_UnitTestCase {
$this->assertSame( $item['uuid'], rest_get_authenticated_app_password() );
}
/**
* @ticket 21022
* @ticket 63203
*
* @covers WP_Application_Passwords::create_new_application_password
*/
public function test_application_password_is_hashed_with_fast_hash() {
// Create a new app-only password.
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$user_id, array( 'name' => 'phpunit' ) );
$this->assertStringStartsWith( '$generic$', $item['password'] );
}
/**
* @ticket 42790
*/
@ -1963,6 +1999,37 @@ class Tests_Auth extends WP_UnitTestCase {
clean_user_cache( $user_id );
}
/**
* Test the tests
*
* @covers Tests_Auth::set_application_password_with_plain_bcrypt
*
* @ticket 21022
* @ticket 63203
*/
public function test_set_application_password_with_plain_bcrypt() {
// Set an application password with the plain_bcrypt algorithm.
$uuid = self::set_application_password_with_plain_bcrypt( 'password', self::$user_id );
// Ensure the password is hashed with plain_bcrypt.
$hash = WP_Application_Passwords::get_user_application_password( self::$user_id, $uuid )['password'];
$this->assertStringStartsWith( '$2y$', $hash );
}
/**
* Creates an application password that is hashed using bcrypt instead of the generic algorithm.
*
* This is ultimately used to mimic a plugged version of `wp_hash_password()` that uses bcrypt and
* facilitate backwards compatibility testing.
*
* @param string $password The password to hash.
* @param int $user_id The user ID to associate the password with.
* @return string The UUID of the application password.
*/
private static function set_application_password_with_plain_bcrypt( string $password, int $user_id ) {
return self::set_application_password( password_hash( $password, PASSWORD_BCRYPT ), $user_id );
}
/**
* Test the tests
*
@ -1979,13 +2046,33 @@ class Tests_Auth extends WP_UnitTestCase {
$this->assertStringStartsWith( '$P$', $hash );
}
/**
* Creates an application password that is hashed using a phpass portable hash instead of the generic algorithm.
*
* This facilitate backwards compatibility testing.
*
* @param string $password The password to hash.
* @param int $user_id The user ID to associate the password with.
* @return string The UUID of the application password.
*/
private static function set_application_password_with_phpass( string $password, int $user_id ) {
return self::set_application_password( self::$wp_hasher->HashPassword( $password ), $user_id );
}
/**
* Creates an application password using the given password hash.
*
* @param string $hash The password hash.
* @param int $user_id The user ID to associate the password with.
* @return string The UUID of the application password.
*/
private static function set_application_password( string $hash, int $user_id ) {
$uuid = wp_generate_uuid4();
$item = array(
'uuid' => $uuid,
'app_id' => '',
'name' => 'Test',
'password' => self::$wp_hasher->HashPassword( $password ),
'password' => $hash,
'created' => time(),
'last_used' => null,
'last_ip' => null,