Skip to content

Commit 61a39de

Browse files
committed
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 git-svn-id: https://develop.svn.wordpress.org/trunk@59803 602fd350-edb4-49c9-b593-d223f7449a82
1 parent d71f29f commit 61a39de

File tree

12 files changed

+131
-165
lines changed

12 files changed

+131
-165
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"issues": "https://core.trac.wordpress.org/"
1111
},
1212
"require": {
13+
"ext-hash": "*",
1314
"ext-json": "*",
1415
"php": ">=7.2.24"
1516
},

src/wp-admin/includes/class-wp-site-health.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -923,7 +923,7 @@ public function get_test_php_extensions() {
923923
),
924924
'hash' => array(
925925
'function' => 'hash',
926-
'required' => false,
926+
'required' => true,
927927
),
928928
'imagick' => array(
929929
'extension' => 'imagick',

src/wp-admin/includes/update-core.php

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,9 +1009,6 @@
10091009
* @global array $_old_requests_files
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.
10171014
* @param string $to Path to old WordPress installation.
@@ -1075,7 +1072,7 @@ function update_core( $from, $to ) {
10751072
}
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
*
10811078
* BC Note: $wp_filesystem->wp_content_dir() returned unslashed pre-2.8.
@@ -1181,17 +1178,29 @@ function update_core( $from, $to ) {
11811178
);
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

11971206
/** This filter is documented in wp-admin/includes/update-core.php */

src/wp-admin/install.php

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,12 +232,13 @@ function display_setup_form( $error = null ) {
232232
}
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.
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.
239240
*/
240-
global $wp_version, $required_php_version, $required_mysql_version, $wpdb;
241+
global $wp_version, $required_php_version, $required_php_extensions, $required_mysql_version, $wpdb;
241242

242243
$php_version = PHP_VERSION;
243244
$mysql_version = $wpdb->db_version();
@@ -298,6 +299,29 @@ function display_setup_form( $error = null ) {
298299
die( '<h1>' . __( 'Requirements Not Met' ) . '</h1><p>' . $compat . '</p></body></html>' );
299300
}
300301

302+
if ( 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+
301325
if ( ! is_string( $wpdb->base_prefix ) || '' === $wpdb->base_prefix ) {
302326
display_header();
303327
die(

src/wp-admin/upgrade.php

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,13 @@
3636
}
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;
45+
global $wp_version, $required_php_version, $required_php_extensions, $required_mysql_version, $wpdb;
4546

4647
$step = (int) $step;
4748

@@ -54,6 +55,24 @@
5455
$mysql_compat = version_compare( $mysql_version, $required_mysql_version, '>=' );
5556
}
5657

58+
$missing_extensions = array();
59+
60+
if ( 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+
}
74+
}
75+
5776
header( 'Content-Type: ' . get_option( 'html_type' ) . '; charset=' . get_option( 'blog_charset' ) );
5877
?>
5978
<!DOCTYPE html>
@@ -126,8 +145,8 @@
126145
}
127146

128147
echo '<p>' . $message . '</p>';
129-
?>
130-
<?php
148+
elseif ( count( $missing_extensions ) > 0 ) :
149+
echo '<p>' . implode( '</p><p>', $missing_extensions ) . '</p>';
131150
else :
132151
switch ( $step ) :
133152
case 0:

src/wp-includes/class-wp-session-tokens.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,7 @@ final public static function get_instance( $user_id ) {
6868
* @return string A hash of the session token (a verifier).
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

7974
/**

src/wp-includes/class-wpdb.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2412,12 +2412,10 @@ public function placeholder_escape() {
24122412
static $placeholder;
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

24232421
/*

src/wp-includes/compat.php

Lines changed: 0 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -263,118 +263,6 @@ function _mb_strlen( $str, $encoding = null ) {
263263
return --$count;
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.
379267
if ( ! function_exists( 'sodium_crypto_box' ) ) {
380268
require ABSPATH . WPINC . '/sodium_compat/autoload.php';

src/wp-includes/load.php

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,12 @@ function wp_populate_basic_auth_from_authorization_header() {
147147
* @since 3.0.0
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
*/
153154
function 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;
157158

@@ -168,6 +169,30 @@ function wp_check_php_mysql_versions() {
168169
exit( 1 );
169170
}
170171

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 );
193+
exit( 1 );
194+
}
195+
171196
// This runs before default constants are defined, so we can't assume WP_CONTENT_DIR is set yet.
172197
$wp_content_dir = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR : ABSPATH . 'wp-content';
173198

src/wp-includes/pluggable.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -772,9 +772,7 @@ function wp_validate_auth_cookie( $cookie = '', $scheme = '' ) {
772772

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 ) ) {
780778
/**
@@ -875,9 +873,7 @@ function wp_generate_auth_cookie( $user_id, $expiration, $scheme = 'auth', $toke
875873

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;
883879

0 commit comments

Comments
 (0)