Make WordPress Core


Ignore:
Timestamp:
04/29/2026 12:26:54 PM (4 months ago)
Author:
audrasjb
Message:

REST API: Harden Real Time Collaboration endpoint.

Adds additional validation and permission checks the the Real Time Collaboration endpoint to ensure only input in the expected format is supported.

Reviewed by peterwilsoncc, audrasjb.
Merges [62198] to the 7.0 branch.
Props czarate, westonruter, joefusco, peterwilsoncc.
Fixes #64890.

Location:
branches/7.0
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • branches/7.0

  • branches/7.0/tests/phpunit/tests/rest-api/rest-sync-server.php

    r62099 r62283  
    1010class WP_Test_REST_Sync_Server extends WP_Test_REST_Controller_Testcase {
    1111
    12         protected static $editor_id;
    13         protected static $subscriber_id;
    14         protected static $post_id;
     12        protected static int $editor_id;
     13        protected static int $subscriber_id;
     14        protected static int $post_id;
     15        protected static int $category_id;
     16        protected static int $tag_id;
     17        protected static int $comment_id;
    1518
    1619        public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
     
    1821                self::$subscriber_id = $factory->user->create( array( 'role' => 'subscriber' ) );
    1922                self::$post_id       = $factory->post->create( array( 'post_author' => self::$editor_id ) );
     23                self::$category_id   = $factory->category->create();
     24                self::$tag_id        = $factory->tag->create();
     25                self::$comment_id    = $factory->comment->create( array( 'comment_post_ID' => self::$post_id ) );
    2026
    2127                // Enable option in setUpBeforeClass to ensure REST routes are registered.
     
    2834                delete_option( 'wp_collaboration_enabled' );
    2935                wp_delete_post( self::$post_id, true );
     36                wp_delete_term( self::$category_id, 'category' );
     37                wp_delete_term( self::$tag_id, 'post_tag' );
     38                wp_delete_comment( self::$comment_id, true );
    3039        }
    3140
     
    278287        }
    279288
     289        /**
     290         * @ticket 64890
     291         */
     292        public function test_sync_malformed_object_id_rejected() {
     293                wp_set_current_user( self::$editor_id );
     294
     295                $response = $this->dispatch_sync( array( $this->build_room( 'postType/post:1abc' ) ) );
     296
     297                $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
     298        }
     299
     300        /**
     301         * @ticket 64890
     302         */
     303        public function test_sync_zero_object_id_rejected(): void {
     304                wp_set_current_user( self::$editor_id );
     305
     306                $response = $this->dispatch_sync( array( $this->build_room( 'postType/post:0' ) ) );
     307
     308                $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
     309        }
     310
     311        /**
     312         * @ticket 64890
     313         */
     314        public function test_sync_post_type_mismatch_rejected(): void {
     315                wp_set_current_user( self::$editor_id );
     316
     317                // The test post is of type 'post', not 'page'.
     318                $response = $this->dispatch_sync( array( $this->build_room( 'postType/page:' . self::$post_id ) ) );
     319
     320                $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
     321        }
     322
     323        /**
     324         * @ticket 64890
     325         */
     326        public function test_sync_taxonomy_term_allowed(): void {
     327                wp_set_current_user( self::$editor_id );
     328
     329                $response = $this->dispatch_sync( array( $this->build_room( 'taxonomy/category:' . self::$category_id ) ) );
     330
     331                $this->assertSame( 200, $response->get_status() );
     332        }
     333
     334        /**
     335         * @ticket 64890
     336         */
     337        public function test_sync_nonexistent_taxonomy_term_rejected(): void {
     338                wp_set_current_user( self::$editor_id );
     339
     340                $response = $this->dispatch_sync( array( $this->build_room( 'taxonomy/category:999999' ) ) );
     341
     342                $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
     343        }
     344
     345        /**
     346         * @ticket 64890
     347         */
     348        public function test_sync_taxonomy_term_wrong_taxonomy_rejected(): void {
     349                wp_set_current_user( self::$editor_id );
     350
     351                // The tag term exists in 'post_tag', not 'category'.
     352                $response = $this->dispatch_sync( array( $this->build_room( 'taxonomy/category:' . self::$tag_id ) ) );
     353
     354                $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
     355        }
     356
     357        /**
     358         * @ticket 64890
     359         */
     360        public function test_sync_comment_allowed(): void {
     361                wp_set_current_user( self::$editor_id );
     362
     363                $response = $this->dispatch_sync( array( $this->build_room( 'root/comment:' . self::$comment_id ) ) );
     364
     365                $this->assertSame( 200, $response->get_status() );
     366        }
     367
     368        /**
     369         * @ticket 64890
     370         */
     371        public function test_sync_nonexistent_comment_rejected(): void {
     372                wp_set_current_user( self::$editor_id );
     373
     374                $response = $this->dispatch_sync( array( $this->build_room( 'root/comment:999999' ) ) );
     375
     376                $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
     377        }
     378
     379        /**
     380         * @ticket 64890
     381         */
     382        public function test_sync_nonexistent_post_type_collection_rejected(): void {
     383                wp_set_current_user( self::$editor_id );
     384
     385                $response = $this->dispatch_sync( array( $this->build_room( 'postType/nonexistent_type' ) ) );
     386
     387                $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
     388        }
     389
    280390        /*
    281391         * Validation tests.
     
    292402
    293403                $this->assertSame( 400, $response->get_status() );
     404        }
     405
     406        /**
     407         * Verifies that schema type validation rejects a non-string value for the
     408         * update 'data' field, confirming that per-arg schema validation still runs
     409         * with a route-level validate_callback registered.
     410         *
     411         * @ticket 64890
     412         */
     413        public function test_sync_rejects_non_string_update_data(): void {
     414                wp_set_current_user( self::$editor_id );
     415
     416                $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' );
     417                $request->set_body_params(
     418                        array(
     419                                'rooms' => array(
     420                                        array(
     421                                                'after'     => 0,
     422                                                'awareness' => array( 'user' => 'test' ),
     423                                                'client_id' => 1,
     424                                                'room'      => $this->get_post_room(),
     425                                                'updates'   => array(
     426                                                        array(
     427                                                                'data' => 12345,
     428                                                                'type' => 'update',
     429                                                        ),
     430                                                ),
     431                                        ),
     432                                ),
     433                        )
     434                );
     435
     436                $response = rest_get_server()->dispatch( $request );
     437                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     438        }
     439
     440        /**
     441         * Verifies that schema enum validation rejects an invalid update type,
     442         * confirming that per-arg schema validation still runs with a route-level
     443         * validate_callback registered.
     444         *
     445         * @ticket 64890
     446         */
     447        public function test_sync_rejects_invalid_update_type_enum(): void {
     448                wp_set_current_user( self::$editor_id );
     449
     450                $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' );
     451                $request->set_body_params(
     452                        array(
     453                                'rooms' => array(
     454                                        array(
     455                                                'after'     => 0,
     456                                                'awareness' => array( 'user' => 'test' ),
     457                                                'client_id' => 1,
     458                                                'room'      => $this->get_post_room(),
     459                                                'updates'   => array(
     460                                                        array(
     461                                                                'data' => 'dGVzdA==',
     462                                                                'type' => 'invalid_type',
     463                                                        ),
     464                                                ),
     465                                        ),
     466                                ),
     467                        )
     468                );
     469
     470                $response = rest_get_server()->dispatch( $request );
     471                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     472        }
     473
     474        /**
     475         * Verifies that schema required-field validation rejects a room missing
     476         * the 'client_id' field, confirming that per-arg schema validation still
     477         * runs with a route-level validate_callback registered.
     478         *
     479         * @ticket 64890
     480         */
     481        public function test_sync_rejects_missing_required_room_field(): void {
     482                wp_set_current_user( self::$editor_id );
     483
     484                $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' );
     485                $request->set_body_params(
     486                        array(
     487                                'rooms' => array(
     488                                        array(
     489                                                'after'     => 0,
     490                                                'awareness' => array( 'user' => 'test' ),
     491                                                // 'client_id' deliberately omitted.
     492                                                'room'      => $this->get_post_room(),
     493                                                'updates'   => array(),
     494                                        ),
     495                                ),
     496                        )
     497                );
     498
     499                $response = rest_get_server()->dispatch( $request );
     500                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     501        }
     502
     503        /**
     504         * Verifies that the maxItems constraint rejects a request with more rooms
     505         * than MAX_ROOMS_PER_REQUEST.
     506         *
     507         * @ticket 64890
     508         */
     509        public function test_sync_rejects_rooms_exceeding_max_items(): void {
     510                wp_set_current_user( self::$editor_id );
     511
     512                $rooms = array();
     513                for ( $i = 0; $i < WP_HTTP_Polling_Sync_Server::MAX_ROOMS_PER_REQUEST + 1; $i++ ) {
     514                        $rooms[] = $this->build_room( 'root/site', $i + 1 );
     515                }
     516
     517                $response = $this->dispatch_sync( $rooms );
     518                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     519        }
     520
     521        /**
     522         * Verifies that the maxLength constraint rejects update data exceeding
     523         * MAX_UPDATE_DATA_SIZE.
     524         *
     525         * @ticket 64890
     526         */
     527        public function test_sync_rejects_update_data_exceeding_max_length(): void {
     528                wp_set_current_user( self::$editor_id );
     529
     530                $oversized_data = str_repeat( 'a', WP_HTTP_Polling_Sync_Server::MAX_UPDATE_DATA_SIZE + 1 );
     531
     532                $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' );
     533                $request->set_body_params(
     534                        array(
     535                                'rooms' => array(
     536                                        array(
     537                                                'after'     => 0,
     538                                                'awareness' => array( 'user' => 'test' ),
     539                                                'client_id' => 1,
     540                                                'room'      => $this->get_post_room(),
     541                                                'updates'   => array(
     542                                                        array(
     543                                                                'data' => $oversized_data,
     544                                                                'type' => 'update',
     545                                                        ),
     546                                                ),
     547                                        ),
     548                                ),
     549                        )
     550                );
     551
     552                $response = rest_get_server()->dispatch( $request );
     553                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     554        }
     555
     556        /**
     557         * Verifies that the route-level validate_callback rejects a request body
     558         * exceeding MAX_BODY_SIZE.
     559         *
     560         * @ticket 64890
     561         */
     562        public function test_sync_rejects_oversized_request_body(): void {
     563                wp_set_current_user( self::$editor_id );
     564
     565                $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' );
     566
     567                // Set valid parsed params so per-arg schema validation passes first.
     568                $request->set_body_params(
     569                        array(
     570                                'rooms' => array(
     571                                        $this->build_room( $this->get_post_room() ),
     572                                ),
     573                        )
     574                );
     575
     576                // Set an oversized raw body to trigger the route-level validate_callback.
     577                $request->set_body( str_repeat( 'x', WP_HTTP_Polling_Sync_Server::MAX_BODY_SIZE + 1 ) );
     578
     579                $response = rest_get_server()->dispatch( $request );
     580                $this->assertErrorResponse( 'rest_sync_body_too_large', $response, 413 );
    294581        }
    295582
Note: See TracChangeset for help on using the changeset viewer.