From df054f2dae7f92ee0f4722798eb6b564fa391286 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Wed, 3 Aug 2022 14:36:12 +0000 Subject: [PATCH] Cache API: Validate cache key in `WP_Object_Cache` methods. Some plugins may call the `wp_cache_*()` functions with an empty string, `false`, or `null` as cache key, usually as a result of not checking the return value of another function that's used as the key. Previously, this was silently failing, leading to odd behavior at best and often breakage due to key collisions. A valid cache key must be either an integer number or a non-empty string. This commit introduces a quick type check on the given cache keys and adds a `_doing_it_wrong()` message that should help plugin developers to notice these issues quicker. Includes: * A check in `update_user_caches()` and `clean_user_cache()` to make sure user email is not empty before being cached or removed from cache, as it is technically possible to create a user with empty email via `wp_insert_user()`. * Some minor cleanup in unit tests where the email was passed to `wp_insert_user()` incorrectly or was unintentionally reset. Props tillkruess, malthert, dd32, spacedmonkey, flixos90, peterwilsoncc, SergeyBiryukov. Fixes #56198. Built from https://develop.svn.wordpress.org/trunk@53818 git-svn-id: http://core.svn.wordpress.org/trunk@53377 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-includes/class-wp-object-cache.php | 68 ++++++++++++++++++++++++++- wp-includes/user.php | 10 +++- wp-includes/version.php | 2 +- 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/wp-includes/class-wp-object-cache.php b/wp-includes/class-wp-object-cache.php index b0c1cc36f5..c22fd6298d 100644 --- a/wp-includes/class-wp-object-cache.php +++ b/wp-includes/class-wp-object-cache.php @@ -129,6 +129,43 @@ class WP_Object_Cache { unset( $this->$name ); } + /** + * Serves as a utility function to determine whether a key is valid. + * + * @since 6.1.0 + * + * @param int|string $key Cache key to check for validity. + * @return bool Whether the key is valid. + */ + protected function is_valid_key( $key ) { + if ( is_int( $key ) ) { + return true; + } + + if ( is_string( $key ) && trim( $key ) !== '' ) { + return true; + } + + $type = gettype( $key ); + + if ( ! function_exists( '__' ) ) { + wp_load_translations_early(); + } + + $message = is_string( $key ) + ? __( 'Cache key must not be an empty string.' ) + /* translators: %s: The type of the given cache key. */ + : sprintf( __( 'Cache key must be integer or non-empty string, %s given.' ), $type ); + + _doing_it_wrong( + sprintf( '%s::%s', __CLASS__, debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 2 )[1]['function'] ), + $message, + '6.1.0' + ); + + return false; + } + /** * Serves as a utility function to determine whether a key exists in the cache. * @@ -163,6 +200,10 @@ class WP_Object_Cache { return false; } + if ( ! $this->is_valid_key( $key ) ) { + return false; + } + if ( empty( $group ) ) { $group = 'default'; } @@ -216,6 +257,10 @@ class WP_Object_Cache { * @return bool True if contents were replaced, false if original value does not exist. */ public function replace( $key, $data, $group = 'default', $expire = 0 ) { + if ( ! $this->is_valid_key( $key ) ) { + return false; + } + if ( empty( $group ) ) { $group = 'default'; } @@ -245,14 +290,19 @@ class WP_Object_Cache { * more for cache plugins which use files. * * @since 2.0.0 + * @since 6.1.0 Returns false if cache key is invalid. * * @param int|string $key What to call the contents in the cache. * @param mixed $data The contents to store in the cache. * @param string $group Optional. Where to group the cache contents. Default 'default'. * @param int $expire Optional. Not used. - * @return true Always returns true. + * @return bool True if contents were set, false if key is invalid. */ public function set( $key, $data, $group = 'default', $expire = 0 ) { + if ( ! $this->is_valid_key( $key ) ) { + return false; + } + if ( empty( $group ) ) { $group = 'default'; } @@ -310,6 +360,10 @@ class WP_Object_Cache { * @return mixed|false The cache contents on success, false on failure to retrieve contents. */ public function get( $key, $group = 'default', $force = false, &$found = null ) { + if ( ! $this->is_valid_key( $key ) ) { + return false; + } + if ( empty( $group ) ) { $group = 'default'; } @@ -368,6 +422,10 @@ class WP_Object_Cache { * @return bool True on success, false if the contents were not deleted. */ public function delete( $key, $group = 'default', $deprecated = false ) { + if ( ! $this->is_valid_key( $key ) ) { + return false; + } + if ( empty( $group ) ) { $group = 'default'; } @@ -416,6 +474,10 @@ class WP_Object_Cache { * @return int|false The item's new value on success, false on failure. */ public function incr( $key, $offset = 1, $group = 'default' ) { + if ( ! $this->is_valid_key( $key ) ) { + return false; + } + if ( empty( $group ) ) { $group = 'default'; } @@ -455,6 +517,10 @@ class WP_Object_Cache { * @return int|false The item's new value on success, false on failure. */ public function decr( $key, $offset = 1, $group = 'default' ) { + if ( ! $this->is_valid_key( $key ) ) { + return false; + } + if ( empty( $group ) ) { $group = 'default'; } diff --git a/wp-includes/user.php b/wp-includes/user.php index 3c0f917bbc..416db2b86f 100644 --- a/wp-includes/user.php +++ b/wp-includes/user.php @@ -1850,8 +1850,11 @@ function update_user_caches( $user ) { wp_cache_add( $user->ID, $user, 'users' ); wp_cache_add( $user->user_login, $user->ID, 'userlogins' ); - wp_cache_add( $user->user_email, $user->ID, 'useremail' ); wp_cache_add( $user->user_nicename, $user->ID, 'userslugs' ); + + if ( ! empty( $user->user_email ) ) { + wp_cache_add( $user->user_email, $user->ID, 'useremail' ); + } } /** @@ -1878,9 +1881,12 @@ function clean_user_cache( $user ) { wp_cache_delete( $user->ID, 'users' ); wp_cache_delete( $user->user_login, 'userlogins' ); - wp_cache_delete( $user->user_email, 'useremail' ); wp_cache_delete( $user->user_nicename, 'userslugs' ); + if ( ! empty( $user->user_email ) ) { + wp_cache_delete( $user->user_email, 'useremail' ); + } + /** * Fires immediately after the given user's cache is cleaned. * diff --git a/wp-includes/version.php b/wp-includes/version.php index 568840a575..dd7119bd2e 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '6.1-alpha-53817'; +$wp_version = '6.1-alpha-53818'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.