Make WordPress Core

Changeset 42000


Ignore:
Timestamp:
10/24/2017 09:04:50 PM (7 years ago)
Author:
joehoyle
Message:

REST API: Don’t remove unregistered properties from objects in schema.

In r41727 the ability to sanitise and validate objects from JSON schema was added, with a whitelist approach. It was decided we should pass through all non-registered properties to reflect the behaviour of the root object in register_rest_route. To prevent arbitrary extra data via setting objects, we force additionalProperties to false in the settings endpoint.

See #38583.

Location:
trunk
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/rest-api.php

    r41744 r42000  
    11071107
    11081108        foreach ( $value as $property => $v ) {
    1109             if ( ! isset( $args['properties'][ $property ] ) ) {
    1110                 continue;
    1111             }
    1112             $is_valid = rest_validate_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' );
    1113 
    1114             if ( is_wp_error( $is_valid ) ) {
    1115                 return $is_valid;
     1109            if ( isset( $args['properties'][ $property ] ) ) {
     1110                $is_valid = rest_validate_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' );
     1111                if ( is_wp_error( $is_valid ) ) {
     1112                    return $is_valid;
     1113                }
     1114            } elseif ( isset( $args['additionalProperties'] ) && false === $args['additionalProperties'] ) {
     1115                return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not a valid property of Object.' ), $property ) );
    11161116            }
    11171117        }
     
    12471247
    12481248        foreach ( $value as $property => $v ) {
    1249             if ( ! isset( $args['properties'][ $property ] ) ) {
     1249            if ( isset( $args['properties'][ $property ] ) ) {
     1250                $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['properties'][ $property ] );
     1251            } elseif ( isset( $args['additionalProperties'] ) && false === $args['additionalProperties'] ) {
    12501252                unset( $value[ $property ] );
    1251                 continue;
    12521253            }
    1253             $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['properties'][ $property ] );
    12541254        }
    12551255
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php

    r41758 r42000  
    546546            }
    547547
    548             foreach ( array( 'type', 'format', 'enum', 'items', 'properties' ) as $schema_prop ) {
     548            foreach ( array( 'type', 'format', 'enum', 'items', 'properties', 'additionalProperties' ) as $schema_prop ) {
    549549                if ( isset( $params[ $schema_prop ] ) ) {
    550550                    $endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ];
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php

    r41758 r42000  
    249249            }
    250250
     251            $rest_args['schema'] = $this->set_additional_properties_to_false( $rest_args['schema'] );
     252
    251253            $rest_options[ $rest_args['name'] ] = $rest_args;
    252254        }
     
    302304        return rest_parse_request_arg( $value, $request, $param );
    303305    }
     306
     307    /**
     308     * Recursively add additionalProperties = false to all objects in a schema.
     309     *
     310     * This is need to restrict properties of objects in settings values to only
     311     * registered items, as the REST API will allow additional properties by
     312     * default.
     313     *
     314     * @since 4.9.0
     315     *
     316     * @param array $schema The schema array.
     317     * @return array
     318     */
     319    protected function set_additional_properties_to_false( $schema ) {
     320        switch ( $schema['type'] ) {
     321            case 'object':
     322                foreach ( $schema['properties'] as $key => $child_schema ) {
     323                    $schema['properties'][ $key ] = $this->set_additional_properties_to_false( $child_schema );
     324                }
     325                $schema['additionalProperties'] = false;
     326                break;
     327            case 'array':
     328                $schema['items'] = $this->set_additional_properties_to_false( $schema['items'] );
     329                break;
     330        }
     331
     332        return $schema;
     333    }
    304334}
  • trunk/tests/phpunit/tests/rest-api/rest-schema-sanitization.php

    r41727 r42000  
    158158        $this->assertEquals( array( 'a' => 1 ), rest_sanitize_value_from_schema( array( 'a' => 1 ), $schema ) );
    159159        $this->assertEquals( array( 'a' => 1 ), rest_sanitize_value_from_schema( array( 'a' => '1' ), $schema ) );
     160        $this->assertEquals( array( 'a' => 1, 'b' => 1 ), rest_sanitize_value_from_schema( array( 'a' => '1', 'b' => 1 ), $schema ) );
     161    }
     162
     163    public function test_type_object_strips_additional_properties() {
     164        $schema = array(
     165            'type'       => 'object',
     166            'properties' => array(
     167                'a' => array(
     168                    'type' => 'number',
     169                ),
     170            ),
     171            'additionalProperties' => false,
     172        );
     173        $this->assertEquals( array( 'a' => 1 ), rest_sanitize_value_from_schema( array( 'a' => 1 ), $schema ) );
     174        $this->assertEquals( array( 'a' => 1 ), rest_sanitize_value_from_schema( array( 'a' => '1' ), $schema ) );
     175        $this->assertEquals( array( 'a' => 1 ), rest_sanitize_value_from_schema( array( 'a' => '1', 'b' => 1 ), $schema ) );
    160176    }
    161177
     
    196212                    'b' => 1,
    197213                    'c' => 3,
    198                 ),
     214                    'd' => '1',
     215                ),
     216                'b' => 1,
    199217            ),
    200218            rest_sanitize_value_from_schema(
  • trunk/tests/phpunit/tests/rest-api/rest-schema-validation.php

    r41727 r42000  
    187187            'properties' => array(
    188188                'a' => array(
    189                     'type' => 'number'
     189                    'type' => 'number',
    190190                ),
    191191            ),
    192192        );
    193193        $this->assertTrue( rest_validate_value_from_schema( array( 'a' => 1 ), $schema ) );
     194        $this->assertTrue( rest_validate_value_from_schema( array( 'a' => 1, 'b' => 2 ), $schema ) );
    194195        $this->assertWPError( rest_validate_value_from_schema( array( 'a' => 'invalid' ), $schema ) );
     196    }
     197
     198    public function test_type_object_additional_properties_false() {
     199        $schema = array(
     200            'type'       => 'object',
     201            'properties' => array(
     202                'a' => array(
     203                    'type' => 'number',
     204                ),
     205            ),
     206            'additionalProperties' => false,
     207        );
     208        $this->assertTrue( rest_validate_value_from_schema( array( 'a' => 1 ), $schema ) );
     209        $this->assertWPError( rest_validate_value_from_schema( array( 'a' => 1, 'b' => 2 ), $schema ) );
    195210    }
    196211
  • trunk/tests/phpunit/tests/rest-api/rest-settings-controller.php

    r41758 r42000  
    191191        ) );
    192192
     193        // We have to re-register the route, as the args changes based off registered settings.
     194        $this->server->override_by_default = true;
     195        $this->endpoint->register_routes();
     196
    193197        // Object is cast to correct types.
    194198        update_option( 'mycustomsetting', array( 'a' => '1' ) );
     
    210214        $response = $this->server->dispatch( $request );
    211215        $data = $response->get_data();
    212         $this->assertEquals( array( 'a' => 1 ), $data['mycustomsetting'] );
     216        $this->assertEquals( null, $data['mycustomsetting'] );
    213217
    214218        unregister_setting( 'somegroup', 'mycustomsetting' );
     
    373377    }
    374378
     379    public function test_update_item_with_nested_object() {
     380        register_setting( 'somegroup', 'mycustomsetting', array(
     381            'show_in_rest' => array(
     382                'schema' => array(
     383                    'type'       => 'object',
     384                    'properties' => array(
     385                        'a' => array(
     386                            'type' => 'object',
     387                            'properties' => array(
     388                                'b' => array(
     389                                    'type' => 'number',
     390                                ),
     391                            ),
     392                        ),
     393                    ),
     394                ),
     395            ),
     396            'type'         => 'object',
     397        ) );
     398
     399        // We have to re-register the route, as the args changes based off registered settings.
     400        $this->server->override_by_default = true;
     401        $this->endpoint->register_routes();
     402        wp_set_current_user( self::$administrator );
     403
     404        $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
     405        $request->set_param( 'mycustomsetting', array( 'a' => array( 'b' => 1, 'c' => 1 ) ) );
     406        $response = $this->server->dispatch( $request );
     407        $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     408    }
     409
    375410    public function test_update_item_with_object() {
    376411        register_setting( 'somegroup', 'mycustomsetting', array(
     
    408443        $this->assertEquals( array(), get_option( 'mycustomsetting' ) );
    409444
     445        // Provide more keys.
     446        $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
     447        $request->set_param( 'mycustomsetting', array( 'a' => 1, 'b' => 2 ) );
     448        $response = $this->server->dispatch( $request );
     449
     450        $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     451
    410452        // Setting an invalid object.
    411453        $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
Note: See TracChangeset for help on using the changeset viewer.