From 690e0ac4a7f449f54ab82bc71c27b537f8ca1f64 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Thu, 8 Oct 2026 07:41:58 +0000 Subject: [PATCH 1/2] Improve PBKDF2 performance This is a security bug, since it makes PBKDF2 faster for attackers than defenders. So it should be fixed from PHP 8.2 on. Fixes https://github.com/php/php-src/issues/9604 --- NEWS | 3 +++ ext/hash/hash.c | 27 ++++++++++++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/NEWS b/NEWS index 91bd9e2da7c4..6886092c9245 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,9 @@ PHP NEWS . Fixed GHSA-62xp-839h-2637 (IPv6 ACL bypass in FastCGI listen.allowed_clients due to partial address comparison). (CVE-2026-91768) (Alexandre Daubois) +- Hash: + . Improved performance of hash_pbkdf2. (Sjoerd Langkemper) + - MySQLnd: . Fixed GHSA-r6x9-5r99-36j7 (Various packet overreads in mysqlnd wire protocol). (CVE-2025-1218) (Jakub Zelenka, Nora Dossche) diff --git a/ext/hash/hash.c b/ext/hash/hash.c index 4fdecfca79fb..f33e69eb96fd 100644 --- a/ext/hash/hash.c +++ b/ext/hash/hash.c @@ -497,6 +497,12 @@ static inline void php_hash_hmac_round(unsigned char *final, const php_hash_ops ops->hash_final(final, context); } +static inline void php_hash_hmac_round_with_copy(unsigned char *final, const php_hash_ops *ops, const void *base_context, void *context, const unsigned char *data, const zend_long data_size) { + ZEND_ASSERT(SUCCESS == ops->hash_copy(ops, base_context, context)); + ops->hash_update(context, data, data_size); + ops->hash_final(final, context); +} + static void php_hash_do_hash_hmac( zval *return_value, zend_string *algo, char *data, size_t data_len, char *key, size_t key_len, bool raw_output, bool isfilename ) /* {{{ */ { @@ -996,7 +1002,7 @@ PHP_FUNCTION(hash_pbkdf2) size_t pass_len, salt_len = 0; bool raw_output = 0; const php_hash_ops *ops; - void *context; + void *context, *inner_context, *outer_context; HashTable *args = NULL; if (zend_parse_parameters(ZEND_NUM_ARGS(), "Sssl|lbh", &algo, &pass, &pass_len, &salt, &salt_len, &iterations, &length, &raw_output, &args) == FAILURE) { @@ -1027,6 +1033,9 @@ PHP_FUNCTION(hash_pbkdf2) context = php_hash_alloc_context(ops); ops->hash_init(context, args); + inner_context = php_hash_alloc_context(ops); + outer_context = php_hash_alloc_context(ops); + K1 = emalloc(ops->block_size); K2 = emalloc(ops->block_size); digest = emalloc(ops->digest_size); @@ -1037,6 +1046,12 @@ PHP_FUNCTION(hash_pbkdf2) /* Convert K1 to opad -- 0x6A = 0x36 ^ 0x5C */ php_hash_string_xor_char(K2, K1, 0x6A, ops->block_size); + /* Precompute the hash states after absorbing the ipad and opad blocks */ + ops->hash_init(inner_context, NULL); + ops->hash_update(inner_context, K1, ops->block_size); + ops->hash_init(outer_context, NULL); + ops->hash_update(outer_context, K2, ops->block_size); + /* Setup Main Loop to build a long enough result */ if (length == 0) { length = ops->digest_size; @@ -1065,8 +1080,8 @@ PHP_FUNCTION(hash_pbkdf2) computed_salt[salt_len + 2] = (unsigned char) ((i & 0xFF00) >> 8); computed_salt[salt_len + 3] = (unsigned char) (i & 0xFF); - php_hash_hmac_round(digest, ops, context, K1, computed_salt, (zend_long) salt_len + 4); - php_hash_hmac_round(digest, ops, context, K2, digest, ops->digest_size); + php_hash_hmac_round_with_copy(digest, ops, inner_context, context, computed_salt, (zend_long) salt_len + 4); + php_hash_hmac_round_with_copy(digest, ops, outer_context, context, digest, ops->digest_size); /* } */ /* temp = digest */ @@ -1078,8 +1093,8 @@ PHP_FUNCTION(hash_pbkdf2) */ for (j = 1; j < iterations; j++) { /* digest = hash_hmac(digest, password) { */ - php_hash_hmac_round(digest, ops, context, K1, digest, ops->digest_size); - php_hash_hmac_round(digest, ops, context, K2, digest, ops->digest_size); + php_hash_hmac_round_with_copy(digest, ops, inner_context, context, digest, ops->digest_size); + php_hash_hmac_round_with_copy(digest, ops, outer_context, context, digest, ops->digest_size); /* } */ /* temp ^= digest */ php_hash_string_xor(temp, temp, digest, ops->digest_size); @@ -1095,6 +1110,8 @@ PHP_FUNCTION(hash_pbkdf2) efree(K2); efree(computed_salt); efree(context); + efree(inner_context); + efree(outer_context); efree(digest); efree(temp); From 5db5b7adbaf29eaf03e58815011fd7d7fc76d1d5 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Thu, 8 Oct 2026 12:11:49 +0000 Subject: [PATCH 2/2] Fix const qualifier warning, don't put side-effect within assert --- ext/hash/hash.c | 4 ++-- ext/hash/php_hash.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/hash/hash.c b/ext/hash/hash.c index f33e69eb96fd..702e2afcb46c 100644 --- a/ext/hash/hash.c +++ b/ext/hash/hash.c @@ -122,7 +122,7 @@ PHP_HASH_API void php_hash_register_algo(const char *algo, const php_hash_ops *o } /* }}} */ -PHP_HASH_API int php_hash_copy(const void *ops, void *orig_context, void *dest_context) /* {{{ */ +PHP_HASH_API int php_hash_copy(const void *ops, const void *orig_context, void *dest_context) /* {{{ */ { php_hash_ops *hash_ops = (php_hash_ops *)ops; @@ -498,7 +498,7 @@ static inline void php_hash_hmac_round(unsigned char *final, const php_hash_ops } static inline void php_hash_hmac_round_with_copy(unsigned char *final, const php_hash_ops *ops, const void *base_context, void *context, const unsigned char *data, const zend_long data_size) { - ZEND_ASSERT(SUCCESS == ops->hash_copy(ops, base_context, context)); + ops->hash_copy(ops, base_context, context); ops->hash_update(context, data, data_size); ops->hash_final(final, context); } diff --git a/ext/hash/php_hash.h b/ext/hash/php_hash.h index 0fd2f5d41f25..17968dd8e2a1 100644 --- a/ext/hash/php_hash.h +++ b/ext/hash/php_hash.h @@ -34,7 +34,7 @@ typedef struct _php_hashcontext_object php_hashcontext_object; typedef void (*php_hash_init_func_t)(void *context, HashTable *args); typedef void (*php_hash_update_func_t)(void *context, const unsigned char *buf, size_t count); typedef void (*php_hash_final_func_t)(unsigned char *digest, void *context); -typedef int (*php_hash_copy_func_t)(const void *ops, void *orig_context, void *dest_context); +typedef int (*php_hash_copy_func_t)(const void *ops, const void *orig_context, void *dest_context); typedef int (*php_hash_serialize_func_t)(const php_hashcontext_object *hash, zend_long *magic, zval *zv); typedef int (*php_hash_unserialize_func_t)(php_hashcontext_object *hash, zend_long magic, const zval *zv); @@ -147,7 +147,7 @@ extern zend_module_entry hash_module_entry; extern PHP_HASH_API zend_class_entry *php_hashcontext_ce; PHP_HASH_API const php_hash_ops *php_hash_fetch_ops(zend_string *algo); PHP_HASH_API void php_hash_register_algo(const char *algo, const php_hash_ops *ops); -PHP_HASH_API int php_hash_copy(const void *ops, void *orig_context, void *dest_context); +PHP_HASH_API int php_hash_copy(const void *ops, const void *orig_context, void *dest_context); PHP_HASH_API int php_hash_serialize(const php_hashcontext_object *context, zend_long *magic, zval *zv); PHP_HASH_API int php_hash_unserialize(php_hashcontext_object *context, zend_long magic, const zval *zv); PHP_HASH_API int php_hash_serialize_spec(const php_hashcontext_object *context, zval *zv, const char *spec);