Make WordPress Core

Changeset 59803


Ignore:
Timestamp:
02/11/2025 11:12:03 AM (6 weeks ago)
Author:
johnbillion
Message:

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

Location:
trunk
Files:
12 edited

Legend:

Unmodified
Added
Removed
  • trunk/composer.json

    r59740 r59803  
    1111    },
    1212    "require": {
     13        "ext-hash": "*",
    1314        "ext-json": "*",
    1415        "php": ">=7.2.24"
  • trunk/src/wp-admin/includes/class-wp-site-health.php

    r59159 r59803  
    924924            'hash'      => array(
    925925                'function' => 'hash',
    926                 'required' => false,
     926                'required' => true,
    927927            ),
    928928            'imagick'   => array(
  • trunk/src/wp-admin/includes/update-core.php

    r59386 r59803  
    10101010 * @global array              $_new_bundled_files
    10111011 * @global wpdb               $wpdb                   WordPress database abstraction object.
    1012  * @global string             $wp_version
    1013  * @global string             $required_php_version
    1014  * @global string             $required_mysql_version
    10151012 *
    10161013 * @param string $from New release unzipped path.
     
    10761073
    10771074    /*
    1078      * Import $wp_version, $required_php_version, and $required_mysql_version from the new version.
     1075     * Import $wp_version, $required_php_version, $required_php_extensions, and $required_mysql_version from the new version.
    10791076     * DO NOT globalize any variables imported from `version-current.php` in this function.
    10801077     *
     
    11821179    }
    11831180
    1184     // Add a warning when the JSON PHP extension is missing.
    1185     if ( ! extension_loaded( 'json' ) ) {
    1186         return new WP_Error(
    1187             'php_not_compatible_json',
    1188             sprintf(
    1189                 /* translators: 1: WordPress version number, 2: The PHP extension name needed. */
    1190                 __( 'The update cannot be installed because WordPress %1$s requires the %2$s PHP extension.' ),
    1191                 $wp_version,
    1192                 'JSON'
    1193             )
    1194         );
     1181    if ( isset( $required_php_extensions ) && is_array( $required_php_extensions ) ) {
     1182        $missing_extensions = new WP_Error();
     1183
     1184        foreach ( $required_php_extensions as $extension ) {
     1185            if ( extension_loaded( $extension ) ) {
     1186                continue;
     1187            }
     1188
     1189            $missing_extensions->add(
     1190                "php_not_compatible_{$extension}",
     1191                sprintf(
     1192                    /* translators: 1: WordPress version number, 2: The PHP extension name needed. */
     1193                    __( 'The update cannot be installed because WordPress %1$s requires the %2$s PHP extension.' ),
     1194                    $wp_version,
     1195                    $extension
     1196                )
     1197            );
     1198        }
     1199
     1200        // Add a warning when required PHP extensions are missing.
     1201        if ( $missing_extensions->has_errors() ) {
     1202            return $missing_extensions;
     1203        }
    11951204    }
    11961205
  • trunk/src/wp-admin/install.php

    r59027 r59803  
    233233
    234234/**
    235  * @global string $wp_version             The WordPress version string.
    236  * @global string $required_php_version   The required PHP version string.
    237  * @global string $required_mysql_version The required MySQL version string.
    238  * @global wpdb   $wpdb                   WordPress database abstraction object.
    239  */
    240 global $wp_version, $required_php_version, $required_mysql_version, $wpdb;
     235 * @global string   $wp_version              The WordPress version string.
     236 * @global string   $required_php_version    The required PHP version string.
     237 * @global string[] $required_php_extensions The names of required PHP extensions.
     238 * @global string   $required_mysql_version  The required MySQL version string.
     239 * @global wpdb     $wpdb                    WordPress database abstraction object.
     240 */
     241global $wp_version, $required_php_version, $required_php_extensions, $required_mysql_version, $wpdb;
    241242
    242243$php_version   = PHP_VERSION;
     
    299300}
    300301
     302if ( isset( $required_php_extensions ) && is_array( $required_php_extensions ) ) {
     303    $missing_extensions = array();
     304
     305    foreach ( $required_php_extensions as $extension ) {
     306        if ( extension_loaded( $extension ) ) {
     307            continue;
     308        }
     309
     310        $missing_extensions[] = sprintf(
     311            /* translators: 1: URL to WordPress release notes, 2: WordPress version number, 3: The PHP extension name needed. */
     312            __( 'You cannot install because <a href="%1$s">WordPress %2$s</a> requires the %3$s PHP extension.' ),
     313            $version_url,
     314            $wp_version,
     315            $extension
     316        );
     317    }
     318
     319    if ( count( $missing_extensions ) > 0 ) {
     320        display_header();
     321        die( '<h1>' . __( 'Requirements Not Met' ) . '</h1><p>' . implode( '</p><p>', $missing_extensions ) . '</p></body></html>' );
     322    }
     323}
     324
    301325if ( ! is_string( $wpdb->base_prefix ) || '' === $wpdb->base_prefix ) {
    302326    display_header();
  • trunk/src/wp-admin/upgrade.php

    r59027 r59803  
    3737
    3838/**
    39  * @global string $wp_version             The WordPress version string.
    40  * @global string $required_php_version   The required PHP version string.
    41  * @global string $required_mysql_version The required MySQL version string.
    42  * @global wpdb   $wpdb                   WordPress database abstraction object.
     39 * @global string   $wp_version              The WordPress version string.
     40 * @global string   $required_php_version    The required PHP version string.
     41 * @global string[] $required_php_extensions The names of required PHP extensions.
     42 * @global string   $required_mysql_version  The required MySQL version string.
     43 * @global wpdb     $wpdb                    WordPress database abstraction object.
    4344 */
    44 global $wp_version, $required_php_version, $required_mysql_version, $wpdb;
     45global $wp_version, $required_php_version, $required_php_extensions, $required_mysql_version, $wpdb;
    4546
    4647$step = (int) $step;
     
    5354} else {
    5455    $mysql_compat = version_compare( $mysql_version, $required_mysql_version, '>=' );
     56}
     57
     58$missing_extensions = array();
     59
     60if ( isset( $required_php_extensions ) && is_array( $required_php_extensions ) ) {
     61    foreach ( $required_php_extensions as $extension ) {
     62        if ( extension_loaded( $extension ) ) {
     63            continue;
     64        }
     65
     66        $missing_extensions[] = sprintf(
     67            /* translators: 1: URL to WordPress release notes, 2: WordPress version number, 3: The PHP extension name needed. */
     68            __( 'You cannot upgrade because <a href="%1$s">WordPress %2$s</a> requires the %3$s PHP extension.' ),
     69            $version_url,
     70            $wp_version,
     71            $extension
     72        );
     73    }
    5574}
    5675
     
    127146
    128147    echo '<p>' . $message . '</p>';
    129     ?>
    130     <?php
     148elseif ( count( $missing_extensions ) > 0 ) :
     149    echo '<p>' . implode( '</p><p>', $missing_extensions ) . '</p>';
    131150else :
    132151    switch ( $step ) :
  • trunk/src/wp-includes/class-wp-session-tokens.php

    r54133 r59803  
    6969     */
    7070    private function hash_token( $token ) {
    71         // If ext/hash is not present, use sha1() instead.
    72         if ( function_exists( 'hash' ) ) {
    73             return hash( 'sha256', $token );
    74         } else {
    75             return sha1( $token );
    76         }
     71        return hash( 'sha256', $token );
    7772    }
    7873
  • trunk/src/wp-includes/class-wpdb.php

    r59754 r59803  
    24132413
    24142414        if ( ! $placeholder ) {
    2415             // If ext/hash is not present, compat.php's hash_hmac() does not support sha256.
    2416             $algo = function_exists( 'hash' ) ? 'sha256' : 'sha1';
    24172415            // Old WP installs may not have AUTH_SALT defined.
    24182416            $salt = defined( 'AUTH_SALT' ) && AUTH_SALT ? AUTH_SALT : (string) rand();
    24192417
    2420             $placeholder = '{' . hash_hmac( $algo, uniqid( $salt, true ), $salt ) . '}';
     2418            $placeholder = '{' . hash_hmac( 'sha256', uniqid( $salt, true ), $salt ) . '}';
    24212419        }
    24222420
  • trunk/src/wp-includes/compat.php

    r59783 r59803  
    264264}
    265265
    266 if ( ! function_exists( 'hash_hmac' ) ) :
    267     /**
    268      * Compat function to mimic hash_hmac().
    269      *
    270      * The Hash extension is bundled with PHP by default since PHP 5.1.2.
    271      * However, the extension may be explicitly disabled on select servers.
    272      * As of PHP 7.4.0, the Hash extension is a core PHP extension and can no
    273      * longer be disabled.
    274      * I.e. when PHP 7.4.0 becomes the minimum requirement, this polyfill
    275      * and the associated `_hash_hmac()` function can be safely removed.
    276      *
    277      * @ignore
    278      * @since 3.2.0
    279      *
    280      * @see _hash_hmac()
    281      *
    282      * @param string $algo   Hash algorithm. Accepts 'md5' or 'sha1'.
    283      * @param string $data   Data to be hashed.
    284      * @param string $key    Secret key to use for generating the hash.
    285      * @param bool   $binary Optional. Whether to output raw binary data (true),
    286      *                       or lowercase hexits (false). Default false.
    287      * @return string|false The hash in output determined by `$binary`.
    288      *                      False if `$algo` is unknown or invalid.
    289      */
    290     function hash_hmac( $algo, $data, $key, $binary = false ) {
    291         return _hash_hmac( $algo, $data, $key, $binary );
    292     }
    293 endif;
    294 
    295 /**
    296  * Internal compat function to mimic hash_hmac().
    297  *
    298  * @ignore
    299  * @since 3.2.0
    300  *
    301  * @param string $algo   Hash algorithm. Accepts 'md5' or 'sha1'.
    302  * @param string $data   Data to be hashed.
    303  * @param string $key    Secret key to use for generating the hash.
    304  * @param bool   $binary Optional. Whether to output raw binary data (true),
    305  *                       or lowercase hexits (false). Default false.
    306  * @return string|false The hash in output determined by `$binary`.
    307  *                      False if `$algo` is unknown or invalid.
    308  */
    309 function _hash_hmac( $algo, $data, $key, $binary = false ) {
    310     $packs = array(
    311         'md5'  => 'H32',
    312         'sha1' => 'H40',
    313     );
    314 
    315     if ( ! isset( $packs[ $algo ] ) ) {
    316         return false;
    317     }
    318 
    319     $pack = $packs[ $algo ];
    320 
    321     if ( strlen( $key ) > 64 ) {
    322         $key = pack( $pack, $algo( $key ) );
    323     }
    324 
    325     $key = str_pad( $key, 64, chr( 0 ) );
    326 
    327     $ipad = ( substr( $key, 0, 64 ) ^ str_repeat( chr( 0x36 ), 64 ) );
    328     $opad = ( substr( $key, 0, 64 ) ^ str_repeat( chr( 0x5C ), 64 ) );
    329 
    330     $hmac = $algo( $opad . pack( $pack, $algo( $ipad . $data ) ) );
    331 
    332     if ( $binary ) {
    333         return pack( $pack, $hmac );
    334     }
    335 
    336     return $hmac;
    337 }
    338 
    339 if ( ! function_exists( 'hash_equals' ) ) :
    340     /**
    341      * Timing attack safe string comparison.
    342      *
    343      * Compares two strings using the same time whether they're equal or not.
    344      *
    345      * Note: It can leak the length of a string when arguments of differing length are supplied.
    346      *
    347      * This function was added in PHP 5.6.
    348      * However, the Hash extension may be explicitly disabled on select servers.
    349      * As of PHP 7.4.0, the Hash extension is a core PHP extension and can no
    350      * longer be disabled.
    351      * I.e. when PHP 7.4.0 becomes the minimum requirement, this polyfill
    352      * can be safely removed.
    353      *
    354      * @since 3.9.2
    355      *
    356      * @param string $known_string Expected string.
    357      * @param string $user_string  Actual, user supplied, string.
    358      * @return bool Whether strings are equal.
    359      */
    360     function hash_equals( $known_string, $user_string ) {
    361         $known_string_length = strlen( $known_string );
    362 
    363         if ( strlen( $user_string ) !== $known_string_length ) {
    364             return false;
    365         }
    366 
    367         $result = 0;
    368 
    369         // Do not attempt to "optimize" this.
    370         for ( $i = 0; $i < $known_string_length; $i++ ) {
    371             $result |= ord( $known_string[ $i ] ) ^ ord( $user_string[ $i ] );
    372         }
    373 
    374         return 0 === $result;
    375     }
    376 endif;
    377 
    378266// sodium_crypto_box() was introduced in PHP 7.2.
    379267if ( ! function_exists( 'sodium_crypto_box' ) ) {
  • trunk/src/wp-includes/load.php

    r59242 r59803  
    148148 * @access private
    149149 *
    150  * @global string $required_php_version The required PHP version string.
    151  * @global string $wp_version           The WordPress version string.
     150 * @global string   $required_php_version    The required PHP version string.
     151 * @global string[] $required_php_extensions The names of required PHP extensions.
     152 * @global string   $wp_version              The WordPress version string.
    152153 */
    153154function wp_check_php_mysql_versions() {
    154     global $required_php_version, $wp_version;
     155    global $required_php_version, $required_php_extensions, $wp_version;
    155156
    156157    $php_version = PHP_VERSION;
     
    166167            $required_php_version
    167168        );
     169        exit( 1 );
     170    }
     171
     172    $missing_extensions = array();
     173
     174    if ( isset( $required_php_extensions ) && is_array( $required_php_extensions ) ) {
     175        foreach ( $required_php_extensions as $extension ) {
     176            if ( extension_loaded( $extension ) ) {
     177                continue;
     178            }
     179
     180            $missing_extensions[] = sprintf(
     181                'WordPress %1$s requires the <code>%2$s</code> PHP extension.',
     182                $wp_version,
     183                $extension
     184            );
     185        }
     186    }
     187
     188    if ( count( $missing_extensions ) > 0 ) {
     189        $protocol = wp_get_server_protocol();
     190        header( sprintf( '%s 500 Internal Server Error', $protocol ), true, 500 );
     191        header( 'Content-Type: text/html; charset=utf-8' );
     192        echo implode( '<br>', $missing_extensions );
    168193        exit( 1 );
    169194    }
  • trunk/src/wp-includes/pluggable.php

    r59754 r59803  
    773773        $key = wp_hash( $username . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme );
    774774
    775         // If ext/hash is not present, compat.php's hash_hmac() does not support sha256.
    776         $algo = function_exists( 'hash' ) ? 'sha256' : 'sha1';
    777         $hash = hash_hmac( $algo, $username . '|' . $expiration . '|' . $token, $key );
     775        $hash = hash_hmac( 'sha256', $username . '|' . $expiration . '|' . $token, $key );
    778776
    779777        if ( ! hash_equals( $hash, $hmac ) ) {
     
    876874        $key = wp_hash( $user->user_login . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme );
    877875
    878         // If ext/hash is not present, compat.php's hash_hmac() does not support sha256.
    879         $algo = function_exists( 'hash' ) ? 'sha256' : 'sha1';
    880         $hash = hash_hmac( $algo, $user->user_login . '|' . $expiration . '|' . $token, $key );
     876        $hash = hash_hmac( 'sha256', $user->user_login . '|' . $expiration . '|' . $token, $key );
    881877
    882878        $cookie = $user->user_login . '|' . $expiration . '|' . $token . '|' . $hash;
  • trunk/src/wp-includes/version.php

    r59275 r59803  
    4141
    4242/**
     43 * Holds the names of required PHP extensions.
     44 *
     45 * @global string[] $required_php_extensions
     46 */
     47$required_php_extensions = array(
     48    'json',
     49    'hash',
     50);
     51
     52/**
    4353 * Holds the required MySQL version.
    4454 *
  • trunk/src/wp-settings.php

    r59670 r59803  
    2323 * these values if already set.
    2424 *
    25  * @global string $wp_version             The WordPress version string.
    26  * @global int    $wp_db_version          WordPress database version.
    27  * @global string $tinymce_version        TinyMCE version.
    28  * @global string $required_php_version   The required PHP version string.
    29  * @global string $required_mysql_version The required MySQL version string.
    30  * @global string $wp_local_package       Locale code of the package.
    31  */
    32 global $wp_version, $wp_db_version, $tinymce_version, $required_php_version, $required_mysql_version, $wp_local_package;
     25 * @global string   $wp_version              The WordPress version string.
     26 * @global int      $wp_db_version           WordPress database version.
     27 * @global string   $tinymce_version         TinyMCE version.
     28 * @global string   $required_php_version    The required PHP version string.
     29 * @global string[] $required_php_extensions The names of required PHP extensions.
     30 * @global string   $required_mysql_version  The required MySQL version string.
     31 * @global string   $wp_local_package        Locale code of the package.
     32 */
     33global $wp_version, $wp_db_version, $tinymce_version, $required_php_version, $required_php_extensions, $required_mysql_version, $wp_local_package;
    3334require ABSPATH . WPINC . '/version.php';
    3435require ABSPATH . WPINC . '/compat.php';
Note: See TracChangeset for help on using the changeset viewer.