Make WordPress Core

Changeset 42000


Ignore:
Timestamp:
10/24/2017 09:04:50 PM (9 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.