Skip to content

Commit ff65b7e

Browse files
committed
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. Props snicco, debarghyabanerjee, peterwilsoncc, jorbin, johnbillion. Fixes #63203 git-svn-id: https://develop.svn.wordpress.org/trunk@60123 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 8eb6456 commit ff65b7e

3 files changed

Lines changed: 98 additions & 3 deletions

File tree

src/wp-includes/class-wp-application-passwords.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,14 @@ public static function check_password(
502502
string $password,
503503
string $hash
504504
): bool {
505+
if ( ! str_starts_with( $hash, '$generic$' ) ) {
506+
/*
507+
* If the hash doesn't start with `$generic$`, it is a hash created with `wp_hash_password()`.
508+
* This is the case for application passwords created before 6.8.0.
509+
*/
510+
return wp_check_password( $password, $hash );
511+
}
512+
505513
return wp_verify_fast_hash( $password, $hash );
506514
}
507515
}

src/wp-includes/functions.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9149,8 +9149,8 @@ function wp_fast_hash(
91499149
* Checks whether a plaintext message matches the hashed value. Used to verify values hashed via wp_fast_hash().
91509150
*
91519151
* The function uses Sodium to hash the message and compare it to the hashed value. If the hash is not a generic hash,
9152-
* the hash is treated as a phpass portable hash in order to provide backward compatibility for application passwords
9153-
* which were hashed using phpass prior to WordPress 6.8.0.
9152+
* the hash is treated as a phpass portable hash in order to provide backward compatibility for passwords and security
9153+
* keys which were hashed using phpass prior to WordPress 6.8.0.
91549154
*
91559155
* @since 6.8.0
91569156
*

tests/phpunit/tests/auth.php

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1134,6 +1134,29 @@ public function test_phpass_application_password_is_accepted() {
11341134
$this->assertSame( self::$user_id, $user->ID );
11351135
}
11361136

1137+
/**
1138+
* @ticket 21022
1139+
* @ticket 63203
1140+
*/
1141+
public function test_plain_bcrypt_application_password_is_accepted() {
1142+
add_filter( 'application_password_is_api_request', '__return_true' );
1143+
add_filter( 'wp_is_application_passwords_available', '__return_true' );
1144+
1145+
$password = 'password';
1146+
1147+
// Set an application password with plain bcrypt, which mimics a password that was hashed with
1148+
// a custom `wp_hash_password()` in use.
1149+
$uuid = self::set_application_password_with_plain_bcrypt( $password, self::$user_id );
1150+
1151+
// Authenticate.
1152+
$user = wp_authenticate_application_password( null, self::USER_LOGIN, $password );
1153+
1154+
// Verify that the plain bcrypt hash for the application password was valid.
1155+
$this->assertNotWPError( $user );
1156+
$this->assertInstanceOf( 'WP_User', $user );
1157+
$this->assertSame( self::$user_id, $user->ID );
1158+
}
1159+
11371160
/**
11381161
* @dataProvider data_usernames
11391162
*
@@ -1591,6 +1614,19 @@ public function test_application_password_authentication() {
15911614
$this->assertSame( $item['uuid'], rest_get_authenticated_app_password() );
15921615
}
15931616

1617+
/**
1618+
* @ticket 21022
1619+
* @ticket 63203
1620+
*
1621+
* @covers WP_Application_Passwords::create_new_application_password
1622+
*/
1623+
public function test_application_password_is_hashed_with_fast_hash() {
1624+
// Create a new app-only password.
1625+
list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$user_id, array( 'name' => 'phpunit' ) );
1626+
1627+
$this->assertStringStartsWith( '$generic$', $item['password'] );
1628+
}
1629+
15941630
/**
15951631
* @ticket 42790
15961632
*/
@@ -1963,6 +1999,37 @@ private static function set_user_password_with_plain_bcrypt( string $password, i
19631999
clean_user_cache( $user_id );
19642000
}
19652001

2002+
/**
2003+
* Test the tests
2004+
*
2005+
* @covers Tests_Auth::set_application_password_with_plain_bcrypt
2006+
*
2007+
* @ticket 21022
2008+
* @ticket 63203
2009+
*/
2010+
public function test_set_application_password_with_plain_bcrypt() {
2011+
// Set an application password with the plain_bcrypt algorithm.
2012+
$uuid = self::set_application_password_with_plain_bcrypt( 'password', self::$user_id );
2013+
2014+
// Ensure the password is hashed with plain_bcrypt.
2015+
$hash = WP_Application_Passwords::get_user_application_password( self::$user_id, $uuid )['password'];
2016+
$this->assertStringStartsWith( '$2y$', $hash );
2017+
}
2018+
2019+
/**
2020+
* Creates an application password that is hashed using bcrypt instead of the generic algorithm.
2021+
*
2022+
* This is ultimately used to mimic a plugged version of `wp_hash_password()` that uses bcrypt and
2023+
* facilitate backwards compatibility testing.
2024+
*
2025+
* @param string $password The password to hash.
2026+
* @param int $user_id The user ID to associate the password with.
2027+
* @return string The UUID of the application password.
2028+
*/
2029+
private static function set_application_password_with_plain_bcrypt( string $password, int $user_id ) {
2030+
return self::set_application_password( password_hash( $password, PASSWORD_BCRYPT ), $user_id );
2031+
}
2032+
19662033
/**
19672034
* Test the tests
19682035
*
@@ -1979,13 +2046,33 @@ public function test_set_application_password_with_phpass() {
19792046
$this->assertStringStartsWith( '$P$', $hash );
19802047
}
19812048

2049+
/**
2050+
* Creates an application password that is hashed using a phpass portable hash instead of the generic algorithm.
2051+
*
2052+
* This facilitate backwards compatibility testing.
2053+
*
2054+
* @param string $password The password to hash.
2055+
* @param int $user_id The user ID to associate the password with.
2056+
* @return string The UUID of the application password.
2057+
*/
19822058
private static function set_application_password_with_phpass( string $password, int $user_id ) {
2059+
return self::set_application_password( self::$wp_hasher->HashPassword( $password ), $user_id );
2060+
}
2061+
2062+
/**
2063+
* Creates an application password using the given password hash.
2064+
*
2065+
* @param string $hash The password hash.
2066+
* @param int $user_id The user ID to associate the password with.
2067+
* @return string The UUID of the application password.
2068+
*/
2069+
private static function set_application_password( string $hash, int $user_id ) {
19832070
$uuid = wp_generate_uuid4();
19842071
$item = array(
19852072
'uuid' => $uuid,
19862073
'app_id' => '',
19872074
'name' => 'Test',
1988-
'password' => self::$wp_hasher->HashPassword( $password ),
2075+
'password' => $hash,
19892076
'created' => time(),
19902077
'last_used' => null,
19912078
'last_ip' => null,

0 commit comments

Comments
 (0)