Skip to content

Commit 29304fb

Browse files
committed
REST API: Fix object/array validation for JSON-encoded GET parameters.This commit aligns GET parameter handling with POST requests by allowingJSON-encoded strings to pass 'object' and 'array' validation andsanitization.- Added JSON coercion in rest_validate_value_from_schema().- Added JSON coercion in rest_sanitize_value_from_schema().- Supports multi-type schemas and uses json_last_error() for safety.Fixes #64926
1 parent 4d3b0b9 commit 29304fb

File tree

2 files changed

+141
-1
lines changed

2 files changed

+141
-1
lines changed

src/wp-includes/rest-api.php

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1845,7 +1845,7 @@ function rest_find_matching_pattern_property_schema( $property, $args ) {
18451845
* @since 5.6.0
18461846
*
18471847
* @param string $param The parameter name.
1848-
* @param array $error The error details.
1848+
* @param array $error The error details.
18491849
* @return WP_Error
18501850
*/
18511851
function rest_format_combining_operation_error( $param, $error ) {
@@ -2182,6 +2182,7 @@ function rest_get_allowed_schema_keywords() {
21822182
* @return true|WP_Error
21832183
*/
21842184
function rest_validate_value_from_schema( $value, $args, $param = '' ) {
2185+
21852186
if ( isset( $args['anyOf'] ) ) {
21862187
$matching_schema = rest_find_any_matching_schema( $value, $args, $param );
21872188
if ( is_wp_error( $matching_schema ) ) {
@@ -2243,9 +2244,32 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) {
22432244
$is_valid = rest_validate_boolean_value_from_schema( $value, $param );
22442245
break;
22452246
case 'object':
2247+
/*
2248+
* A JSON-encoded string (e.g. from a GET query parameter) should be
2249+
* decoded before validation, mirroring what parse_json_params() does
2250+
* for application/json request bodies.
2251+
*/
2252+
if ( is_string( $value ) ) {
2253+
$decoded = json_decode( $value, true );
2254+
if ( null !== $decoded && JSON_ERROR_NONE === json_last_error() ) {
2255+
$value = $decoded;
2256+
}
2257+
}
22462258
$is_valid = rest_validate_object_value_from_schema( $value, $args, $param );
22472259
break;
22482260
case 'array':
2261+
/*
2262+
* A JSON-encoded string (e.g. ?ids=[1,2,3]) should be decoded before
2263+
* validation. This takes priority over the comma-separated-value
2264+
* fallback in rest_is_array() / wp_parse_list(), which cannot
2265+
* preserve value types.
2266+
*/
2267+
if ( is_string( $value ) && str_starts_with( ltrim( $value ), '[' ) ) {
2268+
$decoded = json_decode( $value, true );
2269+
if ( is_array( $decoded ) && JSON_ERROR_NONE === json_last_error() ) {
2270+
$value = $decoded;
2271+
}
2272+
}
22492273
$is_valid = rest_validate_array_value_from_schema( $value, $args, $param );
22502274
break;
22512275
case 'number':
@@ -2780,6 +2804,7 @@ function rest_validate_integer_value_from_schema( $value, $args, $param ) {
27802804
* @return mixed|WP_Error The sanitized value or a WP_Error instance if the value cannot be safely sanitized.
27812805
*/
27822806
function rest_sanitize_value_from_schema( $value, $args, $param = '' ) {
2807+
27832808
if ( isset( $args['anyOf'] ) ) {
27842809
$matching_schema = rest_find_any_matching_schema( $value, $args, $param );
27852810
if ( is_wp_error( $matching_schema ) ) {
@@ -2833,6 +2858,19 @@ function rest_sanitize_value_from_schema( $value, $args, $param = '' ) {
28332858
}
28342859

28352860
if ( 'array' === $args['type'] ) {
2861+
/*
2862+
* A JSON-encoded string (e.g. ?ids=[1,2,3]) should be decoded before
2863+
* sanitization. This takes priority over the comma-separated-value
2864+
* fallback in rest_sanitize_array() / wp_parse_list(), which cannot
2865+
* preserve value types.
2866+
*/
2867+
if ( is_string( $value ) && str_starts_with( ltrim( $value ), '[' ) ) {
2868+
$decoded = json_decode( $value, true );
2869+
if ( is_array( $decoded ) && JSON_ERROR_NONE === json_last_error() ) {
2870+
$value = $decoded;
2871+
}
2872+
}
2873+
28362874
$value = rest_sanitize_array( $value );
28372875

28382876
if ( ! empty( $args['items'] ) ) {
@@ -2850,6 +2888,18 @@ function rest_sanitize_value_from_schema( $value, $args, $param = '' ) {
28502888
}
28512889

28522890
if ( 'object' === $args['type'] ) {
2891+
/*
2892+
* A JSON-encoded string (e.g. from a GET query parameter) should be
2893+
* decoded before sanitization, mirroring what parse_json_params() does
2894+
* for application/json request bodies.
2895+
*/
2896+
if ( is_string( $value ) ) {
2897+
$decoded = json_decode( $value, true );
2898+
if ( null !== $decoded && JSON_ERROR_NONE === json_last_error() ) {
2899+
$value = $decoded;
2900+
}
2901+
}
2902+
28532903
$value = rest_sanitize_object( $value );
28542904

28552905
foreach ( $value as $property => $v ) {

tests/phpunit/tests/rest-api.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2583,4 +2583,94 @@ public function test_should_return_error_if_rest_route_not_string() {
25832583
throw $e; // Re-throw to satisfy expectException
25842584
}
25852585
}
2586+
2587+
2588+
/**
2589+
* @ticket 64926
2590+
* REST API: GET requests fail object/array schema validation
2591+
* when params are JSON-serialized strings.
2592+
* Test that rest_validate_value_from_schema correctly decodes and validates a JSON string for array types.
2593+
*/
2594+
public function test_validate_json_string_array() {
2595+
$schema = array(
2596+
'type' => 'array',
2597+
'items' => array( 'type' => 'integer' ),
2598+
);
2599+
2600+
$value = '[1, 2, 3]';
2601+
$is_valid = rest_validate_value_from_schema( $value, $schema, 'test_param' );
2602+
2603+
$this->assertTrue( $is_valid, 'The JSON array string should be correctly decoded and validated.' );
2604+
}
2605+
2606+
/**
2607+
* @ticket 64926
2608+
* Test that rest_validate_value_from_schema correctly decodes and validates a JSON string for object types.
2609+
*/
2610+
public function test_validate_json_string_object() {
2611+
$schema = array(
2612+
'type' => 'object',
2613+
'properties' => array(
2614+
'id' => array( 'type' => 'integer' ),
2615+
'name' => array( 'type' => 'string' ),
2616+
),
2617+
);
2618+
2619+
$value = '{"id": 123, "name": "Gemini"}';
2620+
$is_valid = rest_validate_value_from_schema( $value, $schema, 'test_param' );
2621+
2622+
$this->assertTrue( $is_valid, 'The JSON object string should be correctly decoded and validated.' );
2623+
}
2624+
2625+
/**
2626+
* @ticket 64926
2627+
* Test that rest_sanitize_value_from_schema correctly decodes and sanitizes a JSON array string.
2628+
*/
2629+
public function test_sanitize_json_string_array() {
2630+
$schema = array(
2631+
'type' => 'array',
2632+
'items' => array( 'type' => 'integer' ),
2633+
);
2634+
2635+
$value = '[10, "20", 30]';
2636+
$sanitized = rest_sanitize_value_from_schema( $value, $schema, 'test_param' );
2637+
2638+
$this->assertIsArray( $sanitized );
2639+
$this->assertSame( array( 10, 20, 30 ), $sanitized );
2640+
}
2641+
2642+
/**
2643+
* @ticket 64926
2644+
* Test that rest_sanitize_value_from_schema correctly decodes and sanitizes a JSON object string.
2645+
*/
2646+
public function test_sanitize_json_string_object() {
2647+
$schema = array(
2648+
'type' => 'object',
2649+
'properties' => array(
2650+
'active' => array( 'type' => 'boolean' ),
2651+
),
2652+
);
2653+
2654+
$value = '{"active": "true"}';
2655+
$sanitized = rest_sanitize_value_from_schema( $value, $schema, 'test_param' );
2656+
2657+
$this->assertIsArray( $sanitized );
2658+
$this->assertTrue( $sanitized['active'] );
2659+
}
2660+
2661+
/**
2662+
* @ticket 64926
2663+
* Test that invalid JSON strings fall back to the original validation logic.
2664+
*/
2665+
public function test_validate_invalid_json_falls_back() {
2666+
$schema = array(
2667+
'type' => 'array',
2668+
);
2669+
2670+
$value = '1,2,3';
2671+
2672+
$is_valid = rest_validate_value_from_schema( $value, $schema, 'test_param' );
2673+
2674+
$this->assertNotWPError( $is_valid );
2675+
}
25862676
}

0 commit comments

Comments
 (0)