Security: Explicitly require the hash PHP extension and add requirement checks during installation and upgrade.

This extension provides the `hash()` function and support for the SHA-256 algorithm, both of which are required for upcoming security related changes. This extension is almost universally enabled, however it is technically possible to disable it on PHP 7.2 and 7.3, hence the introduction of this requirement and the corresponding requirement checks prior to installing or upgrading WordPress.

Props peterwilsoncc, ayeshrajans, dd32, SergeyBiryukov, johnbillion.

Fixes #60638, #62815, #56017

See #21022
Built from https://develop.svn.wordpress.org/trunk@59803


git-svn-id: http://core.svn.wordpress.org/trunk@59145 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
John Blackbourn
2025-02-11 11:14:21 +00:00
parent 9f83001243
commit bb832dcfef
11 changed files with 131 additions and 166 deletions

View File

@@ -923,7 +923,7 @@ class WP_Site_Health {
),
'hash' => array(
'function' => 'hash',
'required' => false,
'required' => true,
),
'imagick' => array(
'extension' => 'imagick',

View File

@@ -1009,9 +1009,6 @@ $_new_bundled_files = array(
* @global array $_old_requests_files
* @global array $_new_bundled_files
* @global wpdb $wpdb WordPress database abstraction object.
* @global string $wp_version
* @global string $required_php_version
* @global string $required_mysql_version
*
* @param string $from New release unzipped path.
* @param string $to Path to old WordPress installation.
@@ -1075,7 +1072,7 @@ function update_core( $from, $to ) {
}
/*
* Import $wp_version, $required_php_version, and $required_mysql_version from the new version.
* Import $wp_version, $required_php_version, $required_php_extensions, and $required_mysql_version from the new version.
* DO NOT globalize any variables imported from `version-current.php` in this function.
*
* BC Note: $wp_filesystem->wp_content_dir() returned unslashed pre-2.8.
@@ -1181,17 +1178,29 @@ function update_core( $from, $to ) {
);
}
// Add a warning when the JSON PHP extension is missing.
if ( ! extension_loaded( 'json' ) ) {
return new WP_Error(
'php_not_compatible_json',
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,
'JSON'
)
);
if ( isset( $required_php_extensions ) && is_array( $required_php_extensions ) ) {
$missing_extensions = new WP_Error();
foreach ( $required_php_extensions as $extension ) {
if ( extension_loaded( $extension ) ) {
continue;
}
$missing_extensions->add(
"php_not_compatible_{$extension}",
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,
$extension
)
);
}
// Add a warning when required PHP extensions are missing.
if ( $missing_extensions->has_errors() ) {
return $missing_extensions;
}
}
/** This filter is documented in wp-admin/includes/update-core.php */

View File

@@ -232,12 +232,13 @@ if ( is_blog_installed() ) {
}
/**
* @global string $wp_version The WordPress version string.
* @global string $required_php_version The required PHP version string.
* @global string $required_mysql_version The required MySQL version string.
* @global wpdb $wpdb WordPress database abstraction object.
* @global string $wp_version The WordPress version string.
* @global string $required_php_version The required PHP version string.
* @global string[] $required_php_extensions The names of required PHP extensions.
* @global string $required_mysql_version The required MySQL version string.
* @global wpdb $wpdb WordPress database abstraction object.
*/
global $wp_version, $required_php_version, $required_mysql_version, $wpdb;
global $wp_version, $required_php_version, $required_php_extensions, $required_mysql_version, $wpdb;
$php_version = PHP_VERSION;
$mysql_version = $wpdb->db_version();
@@ -298,6 +299,29 @@ if ( ! $mysql_compat || ! $php_compat ) {
die( '<h1>' . __( 'Requirements Not Met' ) . '</h1><p>' . $compat . '</p></body></html>' );
}
if ( isset( $required_php_extensions ) && is_array( $required_php_extensions ) ) {
$missing_extensions = array();
foreach ( $required_php_extensions as $extension ) {
if ( extension_loaded( $extension ) ) {
continue;
}
$missing_extensions[] = sprintf(
/* translators: 1: URL to WordPress release notes, 2: WordPress version number, 3: The PHP extension name needed. */
__( 'You cannot install because <a href="%1$s">WordPress %2$s</a> requires the %3$s PHP extension.' ),
$version_url,
$wp_version,
$extension
);
}
if ( count( $missing_extensions ) > 0 ) {
display_header();
die( '<h1>' . __( 'Requirements Not Met' ) . '</h1><p>' . implode( '</p><p>', $missing_extensions ) . '</p></body></html>' );
}
}
if ( ! is_string( $wpdb->base_prefix ) || '' === $wpdb->base_prefix ) {
display_header();
die(

View File

@@ -36,12 +36,13 @@ if ( 'upgrade_db' === $step ) {
}
/**
* @global string $wp_version The WordPress version string.
* @global string $required_php_version The required PHP version string.
* @global string $required_mysql_version The required MySQL version string.
* @global wpdb $wpdb WordPress database abstraction object.
* @global string $wp_version The WordPress version string.
* @global string $required_php_version The required PHP version string.
* @global string[] $required_php_extensions The names of required PHP extensions.
* @global string $required_mysql_version The required MySQL version string.
* @global wpdb $wpdb WordPress database abstraction object.
*/
global $wp_version, $required_php_version, $required_mysql_version, $wpdb;
global $wp_version, $required_php_version, $required_php_extensions, $required_mysql_version, $wpdb;
$step = (int) $step;
@@ -54,6 +55,24 @@ if ( file_exists( WP_CONTENT_DIR . '/db.php' ) && empty( $wpdb->is_mysql ) ) {
$mysql_compat = version_compare( $mysql_version, $required_mysql_version, '>=' );
}
$missing_extensions = array();
if ( isset( $required_php_extensions ) && is_array( $required_php_extensions ) ) {
foreach ( $required_php_extensions as $extension ) {
if ( extension_loaded( $extension ) ) {
continue;
}
$missing_extensions[] = sprintf(
/* translators: 1: URL to WordPress release notes, 2: WordPress version number, 3: The PHP extension name needed. */
__( 'You cannot upgrade because <a href="%1$s">WordPress %2$s</a> requires the %3$s PHP extension.' ),
$version_url,
$wp_version,
$extension
);
}
}
header( 'Content-Type: ' . get_option( 'html_type' ) . '; charset=' . get_option( 'blog_charset' ) );
?>
<!DOCTYPE html>
@@ -126,8 +145,8 @@ elseif ( ! $php_compat || ! $mysql_compat ) :
}
echo '<p>' . $message . '</p>';
?>
<?php
elseif ( count( $missing_extensions ) > 0 ) :
echo '<p>' . implode( '</p><p>', $missing_extensions ) . '</p>';
else :
switch ( $step ) :
case 0:

View File

@@ -68,12 +68,7 @@ abstract class WP_Session_Tokens {
* @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 );
}
/**

View File

@@ -2412,12 +2412,10 @@ class wpdb {
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 ) . '}';
}
/*

View File

@@ -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';

View File

@@ -147,11 +147,12 @@ function wp_populate_basic_auth_from_authorization_header() {
* @since 3.0.0
* @access private
*
* @global string $required_php_version The required PHP version string.
* @global string $wp_version The WordPress version string.
* @global string $required_php_version The required PHP version string.
* @global string[] $required_php_extensions The names of required PHP extensions.
* @global string $wp_version The WordPress version string.
*/
function wp_check_php_mysql_versions() {
global $required_php_version, $wp_version;
global $required_php_version, $required_php_extensions, $wp_version;
$php_version = PHP_VERSION;
@@ -168,6 +169,30 @@ function wp_check_php_mysql_versions() {
exit( 1 );
}
$missing_extensions = array();
if ( isset( $required_php_extensions ) && is_array( $required_php_extensions ) ) {
foreach ( $required_php_extensions as $extension ) {
if ( extension_loaded( $extension ) ) {
continue;
}
$missing_extensions[] = sprintf(
'WordPress %1$s requires the <code>%2$s</code> PHP extension.',
$wp_version,
$extension
);
}
}
if ( count( $missing_extensions ) > 0 ) {
$protocol = wp_get_server_protocol();
header( sprintf( '%s 500 Internal Server Error', $protocol ), true, 500 );
header( 'Content-Type: text/html; charset=utf-8' );
echo implode( '<br>', $missing_extensions );
exit( 1 );
}
// This runs before default constants are defined, so we can't assume WP_CONTENT_DIR is set yet.
$wp_content_dir = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR : ABSPATH . 'wp-content';

View File

@@ -772,9 +772,7 @@ if ( ! function_exists( 'wp_validate_auth_cookie' ) ) :
$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 ) ) {
/**
@@ -875,9 +873,7 @@ if ( ! function_exists( 'wp_generate_auth_cookie' ) ) :
$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;

View File

@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
$wp_version = '6.8-alpha-59802';
$wp_version = '6.8-alpha-59803';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.
@@ -39,6 +39,16 @@ $tinymce_version = '49110-20201110';
*/
$required_php_version = '7.2.24';
/**
* Holds the names of required PHP extensions.
*
* @global string[] $required_php_extensions
*/
$required_php_extensions = array(
'json',
'hash',
);
/**
* Holds the required MySQL version.
*

View File

@@ -22,14 +22,15 @@ define( 'WPINC', 'wp-includes' );
* include version.php from another installation and don't override
* these values if already set.
*
* @global string $wp_version The WordPress version string.
* @global int $wp_db_version WordPress database version.
* @global string $tinymce_version TinyMCE version.
* @global string $required_php_version The required PHP version string.
* @global string $required_mysql_version The required MySQL version string.
* @global string $wp_local_package Locale code of the package.
* @global string $wp_version The WordPress version string.
* @global int $wp_db_version WordPress database version.
* @global string $tinymce_version TinyMCE version.
* @global string $required_php_version The required PHP version string.
* @global string[] $required_php_extensions The names of required PHP extensions.
* @global string $required_mysql_version The required MySQL version string.
* @global string $wp_local_package Locale code of the package.
*/
global $wp_version, $wp_db_version, $tinymce_version, $required_php_version, $required_mysql_version, $wp_local_package;
global $wp_version, $wp_db_version, $tinymce_version, $required_php_version, $required_php_extensions, $required_mysql_version, $wp_local_package;
require ABSPATH . WPINC . '/version.php';
require ABSPATH . WPINC . '/compat.php';
require ABSPATH . WPINC . '/load.php';