diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php index 921360e3ee..6d8239793e 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php @@ -95,6 +95,12 @@ class WP_REST_Settings_Controller extends WP_REST_Controller { * @return mixed */ protected function prepare_value( $value, $schema ) { + // If the value is not a scalar, it's not possible to cast it to + // anything. + if ( ! is_scalar( $value ) ) { + return null; + } + switch ( $schema['type'] ) { case 'string': return (string) $value; @@ -141,9 +147,29 @@ class WP_REST_Settings_Controller extends WP_REST_Controller { continue; } - // A null value means reset the option, which is essentially deleting it - // from the database and then relying on the default value. + /** + * A `null` value for an option would have the same effect as + * deleting the option from the database, and relying on the + * default value. + */ if ( is_null( $request[ $name ] ) ) { + /** + * A `null` value is returned in the response for any option + * that has a non-scalar value. + * + * To protect clients from accidentally including the `null` + * values from a response object in a request, we do not allow + * options with non-scalar values to be updated to `null`. + * Without this added protection a client could mistakenly + * delete all options that have non-scalar values from the + * database. + */ + if ( ! is_scalar( get_option( $args['option_name'], false ) ) ) { + return new WP_Error( + 'rest_invalid_stored_value', sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ), array( 'status' => 500 ) + ); + } + delete_option( $args['option_name'] ); } else { update_option( $args['option_name'], $request[ $name ] ); diff --git a/tests/phpunit/tests/rest-api/rest-settings-controller.php b/tests/phpunit/tests/rest-api/rest-settings-controller.php index 65661e933f..11a20ee5af 100644 --- a/tests/phpunit/tests/rest-api/rest-settings-controller.php +++ b/tests/phpunit/tests/rest-api/rest-settings-controller.php @@ -159,6 +159,51 @@ class WP_Test_REST_Settings_Controller extends WP_Test_REST_Controller_Testcase remove_all_filters( 'rest_pre_get_setting' ); } + public function test_get_item_with_invalid_value_array_in_options() { + wp_set_current_user( self::$administrator ); + + register_setting( 'somegroup', 'mycustomsetting', array( + 'show_in_rest' => array( + 'name' => 'mycustomsettinginrest', + 'schema' => array( + 'enum' => array( 'validvalue1', 'validvalue2' ), + 'default' => 'validvalue1', + ), + ), + 'type' => 'string', + ) ); + + update_option( 'mycustomsetting', array( 'A sneaky array!' ) ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/settings' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( null, $data['mycustomsettinginrest'] ); + } + + public function test_get_item_with_invalid_object_array_in_options() { + wp_set_current_user( self::$administrator ); + + register_setting( 'somegroup', 'mycustomsetting', array( + 'show_in_rest' => array( + 'name' => 'mycustomsettinginrest', + 'schema' => array( + 'enum' => array( 'validvalue1', 'validvalue2' ), + 'default' => 'validvalue1', + ), + ), + 'type' => 'string', + ) ); + + update_option( 'mycustomsetting', (object) array( 'A sneaky array!' ) ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/settings' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( null, $data['mycustomsettinginrest'] ); + } + + public function test_create_item() { } @@ -248,6 +293,23 @@ class WP_Test_REST_Settings_Controller extends WP_Test_REST_Controller_Testcase $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } + public function test_update_item_with_invalid_stored_value_in_options() { + wp_set_current_user( self::$administrator ); + + register_setting( 'somegroup', 'mycustomsetting', array( + 'show_in_rest' => true, + 'type' => 'string', + ) ); + update_option( 'mycustomsetting', array( 'A sneaky array!' ) ); + + wp_set_current_user( self::$administrator ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' ); + $request->set_param( 'mycustomsetting', null ); + $response = $this->server->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_stored_value', $response, 500 ); + } + public function test_delete_item() { }