From 03766dd4a4ca20641bf46f9a18d1a78ca19e7d7a Mon Sep 17 00:00:00 2001 From: kalpeshhiran Date: Thu, 9 Apr 2026 11:38:59 +0530 Subject: [PATCH] Options: Return default value when option data cannot be unserialized. When an option exists in the database but its serialized value is corrupted, maybe_unserialize() returns false, which get_option() incorrectly returned instead of the caller's $default_value. Fixes #59588. --- src/wp-includes/option.php | 18 ++++++- tests/phpunit/tests/option/option.php | 69 +++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php index c4576187dd80b..e4caed65bfc35 100644 --- a/src/wp-includes/option.php +++ b/src/wp-includes/option.php @@ -240,6 +240,22 @@ function get_option( $option, $default_value = false ) { $value = untrailingslashit( $value ); } + $raw_value = $value; + $value = maybe_unserialize( $value ); + + /* + * If the stored value was serialized but could not be unserialized, `maybe_unserialize()` + * returns `false`. In that case, return the default value to avoid incorrectly returning + * `false` instead of the caller's expected default. + * + * Note: `b:0;` is the valid serialization of boolean `false`. When properly unserialized + * it returns `false`, which is correct and should not be treated as a failure. + */ + if ( false === $value && is_serialized( $raw_value ) && 'b:0;' !== trim( $raw_value ) ) { + /** This filter is documented in wp-includes/option.php */ + return apply_filters( "default_option_{$option}", $default_value, $option, $passed_default ); + } + /** * Filters the value of an existing option. * @@ -253,7 +269,7 @@ function get_option( $option, $default_value = false ) { * unserialized prior to being returned. * @param string $option Option name. */ - return apply_filters( "option_{$option}", maybe_unserialize( $value ), $option ); + return apply_filters( "option_{$option}", $value, $option ); } /** diff --git a/tests/phpunit/tests/option/option.php b/tests/phpunit/tests/option/option.php index 53cc5e75f4d31..6c705a7aec789 100644 --- a/tests/phpunit/tests/option/option.php +++ b/tests/phpunit/tests/option/option.php @@ -161,6 +161,75 @@ public function test_serialized_data() { $this->assertTrue( delete_option( $key ) ); } + /** + * Tests that get_option() returns the default value when stored data is corrupted/unserializable. + * + * @ticket 59588 + * + * @covers ::get_option + */ + public function test_get_option_returns_default_when_unserialize_fails() { + global $wpdb; + + // Insert a corrupted serialized value directly into the DB. + $option_name = 'test_corrupted_option_59588'; + $wpdb->insert( + $wpdb->options, + array( + 'option_name' => $option_name, + 'option_value' => 'a:2:{i:0;s:3:"foo";', // Truncated/corrupted serialized array. + 'autoload' => 'yes', + ) + ); + // Clear alloptions cache so get_option() reads from DB. + wp_cache_delete( 'alloptions', 'options' ); + wp_cache_delete( $option_name, 'options' ); + + $default = array( 'default_key' => 'default_value' ); + $result = get_option( $option_name, $default ); + + // Cleanup. + $wpdb->delete( $wpdb->options, array( 'option_name' => $option_name ) ); + wp_cache_delete( 'alloptions', 'options' ); + wp_cache_delete( $option_name, 'options' ); + + $this->assertSame( $default, $result, 'get_option() should return the default value when stored data cannot be unserialized.' ); + } + + /** + * Tests that get_option() correctly returns false when the stored value is a serialized boolean false. + * + * @ticket 59588 + * + * @covers ::get_option + */ + public function test_get_option_returns_false_for_serialized_false_value() { + global $wpdb; + + // Insert the serialization of boolean false (`b:0;`) directly into the DB. + $option_name = 'test_serialized_false_59588'; + $wpdb->insert( + $wpdb->options, + array( + 'option_name' => $option_name, + 'option_value' => 'b:0;', + 'autoload' => 'yes', + ) + ); + // Clear alloptions cache so get_option() reads from DB. + wp_cache_delete( 'alloptions', 'options' ); + wp_cache_delete( $option_name, 'options' ); + + $result = get_option( $option_name, 'should-not-be-returned' ); + + // Cleanup. + $wpdb->delete( $wpdb->options, array( 'option_name' => $option_name ) ); + wp_cache_delete( 'alloptions', 'options' ); + wp_cache_delete( $option_name, 'options' ); + + $this->assertFalse( $result, 'get_option() should return false when the stored value is a serialized boolean false.' ); + } + /** * @ticket 23289 *