From 6e66fd2263a1081060bb2f312524979e7be7eb5a Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Thu, 16 Jan 2025 17:53:41 +0100 Subject: [PATCH] Explicitly require the `hash` extension. --- composer.json | 1 + .../includes/class-wp-site-health.php | 2 +- src/wp-admin/includes/update-core.php | 13 ++ src/wp-includes/class-wp-session-tokens.php | 7 +- src/wp-includes/class-wpdb.php | 4 +- src/wp-includes/compat.php | 112 ------------------ src/wp-includes/pluggable.php | 8 +- tests/phpunit/tests/compat/hashHmac.php | 65 ---------- 8 files changed, 19 insertions(+), 193 deletions(-) delete mode 100644 tests/phpunit/tests/compat/hashHmac.php diff --git a/composer.json b/composer.json index eb78d144e590c..76b0b9b3991d6 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "issues": "https://core.trac.wordpress.org/" }, "require": { + "ext-hash": "*", "ext-json": "*", "php": ">=7.2.24" }, diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index 3f89fc0a38ffc..19d109d447283 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -923,7 +923,7 @@ public function get_test_php_extensions() { ), 'hash' => array( 'function' => 'hash', - 'required' => false, + 'required' => true, ), 'imagick' => array( 'extension' => 'imagick', diff --git a/src/wp-admin/includes/update-core.php b/src/wp-admin/includes/update-core.php index a29f389860127..0155fc1f93891 100644 --- a/src/wp-admin/includes/update-core.php +++ b/src/wp-admin/includes/update-core.php @@ -1194,6 +1194,19 @@ function update_core( $from, $to ) { ); } + // Add a warning when the hash PHP extension is missing (only affects PHP < 7.4). + if ( ! extension_loaded( 'hash' ) ) { + return new WP_Error( + 'php_not_compatible_hash', + sprintf( + /* translators: 1: WordPress version number, 2: The PHP extension name needed. */ + __( 'The update cannot be installed because WordPress %1$s requires the %2$s PHP extension.' ), + $wp_version, + 'hash' + ) + ); + } + /** This filter is documented in wp-admin/includes/update-core.php */ apply_filters( 'update_feedback', __( 'Preparing to install the latest version…' ) ); diff --git a/src/wp-includes/class-wp-session-tokens.php b/src/wp-includes/class-wp-session-tokens.php index feabf698b7467..9482e1b948777 100644 --- a/src/wp-includes/class-wp-session-tokens.php +++ b/src/wp-includes/class-wp-session-tokens.php @@ -68,12 +68,7 @@ final public static function get_instance( $user_id ) { * @return string A hash of the session token (a verifier). */ private function hash_token( $token ) { - // If ext/hash is not present, use sha1() instead. - if ( function_exists( 'hash' ) ) { - return hash( 'sha256', $token ); - } else { - return sha1( $token ); - } + return hash( 'sha256', $token ); } /** diff --git a/src/wp-includes/class-wpdb.php b/src/wp-includes/class-wpdb.php index 69b934ae38ed2..62aac26252668 100644 --- a/src/wp-includes/class-wpdb.php +++ b/src/wp-includes/class-wpdb.php @@ -2406,12 +2406,10 @@ public function placeholder_escape() { static $placeholder; if ( ! $placeholder ) { - // If ext/hash is not present, compat.php's hash_hmac() does not support sha256. - $algo = function_exists( 'hash' ) ? 'sha256' : 'sha1'; // Old WP installs may not have AUTH_SALT defined. $salt = defined( 'AUTH_SALT' ) && AUTH_SALT ? AUTH_SALT : (string) rand(); - $placeholder = '{' . hash_hmac( $algo, uniqid( $salt, true ), $salt ) . '}'; + $placeholder = '{' . hash_hmac( 'sha256', uniqid( $salt, true ), $salt ) . '}'; } /* diff --git a/src/wp-includes/compat.php b/src/wp-includes/compat.php index 6a393f7c984e3..b728e6bc77dea 100644 --- a/src/wp-includes/compat.php +++ b/src/wp-includes/compat.php @@ -263,118 +263,6 @@ function _mb_strlen( $str, $encoding = null ) { return --$count; } -if ( ! function_exists( 'hash_hmac' ) ) : - /** - * Compat function to mimic hash_hmac(). - * - * The Hash extension is bundled with PHP by default since PHP 5.1.2. - * However, the extension may be explicitly disabled on select servers. - * As of PHP 7.4.0, the Hash extension is a core PHP extension and can no - * longer be disabled. - * I.e. when PHP 7.4.0 becomes the minimum requirement, this polyfill - * and the associated `_hash_hmac()` function can be safely removed. - * - * @ignore - * @since 3.2.0 - * - * @see _hash_hmac() - * - * @param string $algo Hash algorithm. Accepts 'md5' or 'sha1'. - * @param string $data Data to be hashed. - * @param string $key Secret key to use for generating the hash. - * @param bool $binary Optional. Whether to output raw binary data (true), - * or lowercase hexits (false). Default false. - * @return string|false The hash in output determined by `$binary`. - * False if `$algo` is unknown or invalid. - */ - function hash_hmac( $algo, $data, $key, $binary = false ) { - return _hash_hmac( $algo, $data, $key, $binary ); - } -endif; - -/** - * Internal compat function to mimic hash_hmac(). - * - * @ignore - * @since 3.2.0 - * - * @param string $algo Hash algorithm. Accepts 'md5' or 'sha1'. - * @param string $data Data to be hashed. - * @param string $key Secret key to use for generating the hash. - * @param bool $binary Optional. Whether to output raw binary data (true), - * or lowercase hexits (false). Default false. - * @return string|false The hash in output determined by `$binary`. - * False if `$algo` is unknown or invalid. - */ -function _hash_hmac( $algo, $data, $key, $binary = false ) { - $packs = array( - 'md5' => 'H32', - 'sha1' => 'H40', - ); - - if ( ! isset( $packs[ $algo ] ) ) { - return false; - } - - $pack = $packs[ $algo ]; - - if ( strlen( $key ) > 64 ) { - $key = pack( $pack, $algo( $key ) ); - } - - $key = str_pad( $key, 64, chr( 0 ) ); - - $ipad = ( substr( $key, 0, 64 ) ^ str_repeat( chr( 0x36 ), 64 ) ); - $opad = ( substr( $key, 0, 64 ) ^ str_repeat( chr( 0x5C ), 64 ) ); - - $hmac = $algo( $opad . pack( $pack, $algo( $ipad . $data ) ) ); - - if ( $binary ) { - return pack( $pack, $hmac ); - } - - return $hmac; -} - -if ( ! function_exists( 'hash_equals' ) ) : - /** - * Timing attack safe string comparison. - * - * Compares two strings using the same time whether they're equal or not. - * - * Note: It can leak the length of a string when arguments of differing length are supplied. - * - * This function was added in PHP 5.6. - * However, the Hash extension may be explicitly disabled on select servers. - * As of PHP 7.4.0, the Hash extension is a core PHP extension and can no - * longer be disabled. - * I.e. when PHP 7.4.0 becomes the minimum requirement, this polyfill - * can be safely removed. - * - * @since 3.9.2 - * - * @param string $known_string Expected string. - * @param string $user_string Actual, user supplied, string. - * @return bool Whether strings are equal. - */ - function hash_equals( $known_string, $user_string ) { - $known_string_length = strlen( $known_string ); - - if ( strlen( $user_string ) !== $known_string_length ) { - return false; - } - - $result = 0; - - // Do not attempt to "optimize" this. - for ( $i = 0; $i < $known_string_length; $i++ ) { - $result |= ord( $known_string[ $i ] ) ^ ord( $user_string[ $i ] ); - } - - return 0 === $result; - } -endif; - // sodium_crypto_box() was introduced in PHP 7.2. if ( ! function_exists( 'sodium_crypto_box' ) ) { require ABSPATH . WPINC . '/sodium_compat/autoload.php'; diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index f4a8d8412e88c..c689a8b82aa22 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -768,9 +768,7 @@ function wp_validate_auth_cookie( $cookie = '', $scheme = '' ) { $key = wp_hash( $username . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme ); - // If ext/hash is not present, compat.php's hash_hmac() does not support sha256. - $algo = function_exists( 'hash' ) ? 'sha256' : 'sha1'; - $hash = hash_hmac( $algo, $username . '|' . $expiration . '|' . $token, $key ); + $hash = hash_hmac( 'sha256', $username . '|' . $expiration . '|' . $token, $key ); if ( ! hash_equals( $hash, $hmac ) ) { /** @@ -871,9 +869,7 @@ function wp_generate_auth_cookie( $user_id, $expiration, $scheme = 'auth', $toke $key = wp_hash( $user->user_login . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme ); - // If ext/hash is not present, compat.php's hash_hmac() does not support sha256. - $algo = function_exists( 'hash' ) ? 'sha256' : 'sha1'; - $hash = hash_hmac( $algo, $user->user_login . '|' . $expiration . '|' . $token, $key ); + $hash = hash_hmac( 'sha256', $user->user_login . '|' . $expiration . '|' . $token, $key ); $cookie = $user->user_login . '|' . $expiration . '|' . $token . '|' . $hash; diff --git a/tests/phpunit/tests/compat/hashHmac.php b/tests/phpunit/tests/compat/hashHmac.php deleted file mode 100644 index f9a61d5faca63..0000000000000 --- a/tests/phpunit/tests/compat/hashHmac.php +++ /dev/null @@ -1,65 +0,0 @@ -assertTrue( function_exists( 'hash_hmac' ) ); - } - - public function test_hash_hmac_simple() { - $data = 'simple'; - $key = 'key'; - - $this->assertSame( - '140d1cb79fa12e2a31f32d35ad0a2723', - _hash_hmac( 'md5', $data, $key ), - 'MD5 hash does not match' - ); - $this->assertSame( - '993003b95758e0ac2eba451a4c5877eb1bb7b92a', - _hash_hmac( 'sha1', $data, $key ), - 'sha1 hash does not match' - ); - } - - public function test_hash_hmac_padding() { - $data = 'simple'; - $key = '65 character key 65 character key 65 character key 65 character k'; - - $this->assertSame( - '3c1399103807cf12ec38228614416a8c', - _hash_hmac( 'md5', $data, $key ), - 'MD5 hash does not match' - ); - $this->assertSame( - '4428826d20003e309d6c2a6515891370daf184ea', - _hash_hmac( 'sha1', $data, $key ), - 'sha1 hash does not match' - ); - } - - public function test_hash_hmac_output() { - $data = 'simple'; - $key = 'key'; - - $this->assertSame( - array( 1 => '140d1cb79fa12e2a31f32d35ad0a2723' ), - unpack( 'H32', _hash_hmac( 'md5', $data, $key, true ) ), - 'unpacked MD5 hash does not match' - ); - $this->assertSame( - array( 1 => '993003b95758e0ac2eba451a4c5877eb1bb7b92a' ), - unpack( 'H40', _hash_hmac( 'sha1', $data, $key, true ) ), - 'unpacked sha1 hash does not match' - ); - } -}