From 6f96d20519e95600ec35c07ad43527f919dcc24b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 6 Feb 2025 10:21:10 -0800 Subject: [PATCH] Add server-side constraint to limit request body to 64 KiB --- .../storage/rest-api.php | 21 ++ .../tests/storage/test-rest-api.php | 336 +++++++++++++----- 2 files changed, 262 insertions(+), 95 deletions(-) diff --git a/plugins/optimization-detective/storage/rest-api.php b/plugins/optimization-detective/storage/rest-api.php index bf1aef27eb..8f4c390a8e 100644 --- a/plugins/optimization-detective/storage/rest-api.php +++ b/plugins/optimization-detective/storage/rest-api.php @@ -214,6 +214,27 @@ function od_handle_rest_request( WP_REST_Request $request ) { ); } + /* + * The limit for data sent via navigator.sendBeacon() is 64 KiB. This limit is checked in detect.js so that the + * request will not even be attempted if the payload is too large. This server-side restriction is added as a + * safeguard against clients sending possibly malicious payloads much larger than 64 KiB which should never be + * getting sent. + */ + $max_size = 64 * 1024; + $content_length = strlen( (string) wp_json_encode( $url_metric ) ); + if ( $content_length > $max_size ) { + return new WP_Error( + 'rest_content_too_large', + sprintf( + /* translators: 1: the size of the payload, 2: the maximum allowed payload size */ + __( 'JSON payload size is %1$s bytes which is larger than the maximum allowed size of %2$s bytes.', 'optimization-detective' ), + number_format_i18n( $content_length ), + number_format_i18n( $max_size ) + ), + array( 'status' => 413 ) + ); + } + // TODO: This should be changed from store_url_metric($slug, $url_metric) instead be update_post( $slug, $group_collection ). As it stands, store_url_metric() is duplicating logic here. $result = OD_URL_Metrics_Post_Type::store_url_metric( $request->get_param( 'slug' ), diff --git a/plugins/optimization-detective/tests/storage/test-rest-api.php b/plugins/optimization-detective/tests/storage/test-rest-api.php index 64213615ac..c369955711 100644 --- a/plugins/optimization-detective/tests/storage/test-rest-api.php +++ b/plugins/optimization-detective/tests/storage/test-rest-api.php @@ -173,123 +173,269 @@ public function data_provider_invalid_params(): array { $valid_params = $this->get_valid_params(); $valid_element = $valid_params['elements'][0]; - return array_map( - static function ( $params ) use ( $valid_params ) { - return array( - 'params' => array_merge( $valid_params, $params ), - ); - }, - array( - 'bad_url' => array( - 'url' => 'bad://url', + return array( + 'missing_callback_params' => array( + 'params' => array( + 'slug' => $valid_params['slug'], ), - 'bad_current_etag1' => array( - 'current_etag' => 'foo', + 'expected_status' => 400, + 'expected_code' => 'rest_missing_callback_param', + ), + 'bad_url' => array( + 'params' => array_merge( + $valid_params, + array( + 'url' => 'bad://url', + ) ), - 'bad_current_etag2' => array( - 'current_etag' => $valid_params['current_etag'] . "\n", + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), + 'bad_current_etag1' => array( + 'params' => array_merge( + $valid_params, + array( + 'current_etag' => 'foo', + ) ), - 'bad_slug' => array( - 'slug' => '', + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), + 'bad_current_etag2' => array( + 'params' => array_merge( + $valid_params, + array( + 'current_etag' => $valid_params['current_etag'] . "\n", + ) ), - 'bad_hmac' => array( - 'hmac' => 'not even a hash', + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), + 'bad_slug' => array( + 'params' => array_merge( + $valid_params, + array( + 'slug' => '', + ) ), - 'invalid_hmac' => array( - 'hmac' => od_get_url_metrics_storage_hmac( od_get_url_metrics_slug( array( 'different' => 'query vars' ) ), $valid_params['current_etag'], home_url( '/' ) ), + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), + 'bad_hmac' => array( + 'params' => array_merge( + $valid_params, + array( + 'hmac' => 'not even a hash', + ) ), - 'invalid_hmac_with_queried_object' => array( - 'hmac' => od_get_url_metrics_storage_hmac( od_get_url_metrics_slug( array() ), $valid_params['current_etag'], home_url( '/' ), 1 ), + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), + 'invalid_hmac' => array( + 'params' => array_merge( + $valid_params, + array( + 'hmac' => od_get_url_metrics_storage_hmac( od_get_url_metrics_slug( array( 'different' => 'query vars' ) ), $valid_params['current_etag'], home_url( '/' ) ), + ) ), - 'invalid_viewport_type' => array( - 'viewport' => '640x480', + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), + 'invalid_hmac_with_queried_object' => array( + 'params' => array_merge( + $valid_params, + array( + 'hmac' => od_get_url_metrics_storage_hmac( od_get_url_metrics_slug( array() ), $valid_params['current_etag'], home_url( '/' ), 1 ), + ) ), - 'invalid_viewport_values' => array( - 'viewport' => array( - 'breadth' => 100, - 'depth' => 200, - ), + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), + 'invalid_viewport_type' => array( + 'params' => array_merge( + $valid_params, + array( + 'viewport' => '640x480', + ) ), - 'invalid_viewport_aspect_ratio' => array( - 'viewport' => array( - 'width' => 1024, - 'height' => 12000, - ), + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), + 'invalid_viewport_values' => array( + 'params' => array_merge( + $valid_params, + array( + 'viewport' => array( + 'breadth' => 100, + 'depth' => 200, + ), + ) ), - 'invalid_elements_type' => array( - 'elements' => 'bad', + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), + 'invalid_viewport_aspect_ratio' => array( + 'params' => array_merge( + $valid_params, + array( + 'viewport' => array( + 'width' => 1024, + 'height' => 12000, + ), + ) ), - 'invalid_elements_prop_is_lcp' => array( - 'elements' => array( - array_merge( - $valid_element, - array( - 'isLCP' => 'totally!', - ) + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), + 'invalid_elements_type' => array( + 'params' => array_merge( + $valid_params, + array( + 'elements' => 'bad', + ) + ), + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), + 'invalid_elements_prop_is_lcp' => array( + 'params' => array_merge( + $valid_params, + array( + 'elements' => array( + array_merge( + $valid_element, + array( + 'isLCP' => 'totally!', + ) + ), ), - ), + ) ), - 'invalid_elements_prop_xpath' => array( - 'elements' => array( - array_merge( - $valid_element, - array( - 'xpath' => 'html > body img', - ) + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), + 'invalid_elements_prop_xpath' => array( + 'params' => array_merge( + $valid_params, + array( + 'elements' => array( + array_merge( + $valid_element, + array( + 'xpath' => 'html > body img', + ) + ), ), - ), + ) ), - 'invalid_elements_prop_intersection_ratio' => array( - 'elements' => array( - array_merge( - $valid_element, - array( - 'intersectionRatio' => - 1, + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), + 'invalid_content_length' => array( + 'params' => array_merge( + $valid_params, + array( + // Repeat the elements until the JSON will surpass 64 KiB. + 'elements' => array_fill( + 0, + 200, + array_merge( + $valid_element, + array( + 'xpath' => '/HTML/BODY/DIV[@id=\'page\']/*[1][self::DIV]', + ) ) ), - ), + ) ), - 'invalid_elements_additional_intersect_rect_property' => array( - 'elements' => array( - array_merge( - $valid_element, - array( - 'intersectionRect' => array( - 'width' => 640, - 'height' => 480, - 'wooHoo' => 'bad', - ), - ) + 'expected_status' => 413, + 'expected_code' => 'rest_content_too_large', + ), + 'invalid_elements_prop_intersection_ratio' => array( + 'params' => array_merge( + $valid_params, + array( + 'elements' => array( + array_merge( + $valid_element, + array( + 'intersectionRatio' => - 1, + ) + ), ), - ), + ) ), - 'invalid_elements_negative_width_intersect_rect_property' => array( - 'elements' => array( - array_merge( - $valid_element, - array( - 'intersectionRect' => array( - 'width' => -640, - 'height' => 480, - ), - ) + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), + 'invalid_elements_additional_intersect_rect_property' => array( + 'params' => array_merge( + $valid_params, + array( + 'elements' => array( + array_merge( + $valid_element, + array( + 'intersectionRect' => array( + 'width' => 640, + 'height' => 480, + 'wooHoo' => 'bad', + ), + ) + ), ), - ), + ) ), - 'invalid_root_property' => array( - 'is_touch' => false, + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), + 'invalid_elements_negative_width_intersect_rect_property' => array( + 'params' => array_merge( + $valid_params, + array( + 'elements' => array( + array_merge( + $valid_element, + array( + 'intersectionRect' => array( + 'width' => -640, + 'height' => 480, + ), + ) + ), + ), + ) ), - 'invalid_element_property' => array( - 'elements' => array( - array_merge( - $valid_element, - array( - 'is_big' => true, - ) + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), + 'invalid_root_property' => array( + 'params' => array_merge( + $valid_params, + array( + 'is_touch' => false, + ) + ), + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), + 'invalid_element_property' => array( + 'params' => array_merge( + $valid_params, + array( + 'elements' => array( + array_merge( + $valid_element, + array( + 'is_big' => true, + ) + ), ), - ), + ) ), - ) + 'expected_status' => 400, + 'expected_code' => 'rest_invalid_param', + ), ); } @@ -304,11 +450,11 @@ static function ( $params ) use ( $valid_params ) { * * @param array $params Params. */ - public function test_rest_request_bad_params( array $params ): void { + public function test_rest_request_bad_params( array $params, int $expected_status, string $expected_code ): void { $request = $this->create_request( $params ); $response = rest_get_server()->dispatch( $request ); - $this->assertSame( 400, $response->get_status(), 'Response: ' . wp_json_encode( $response ) ); - $this->assertSame( 'rest_invalid_param', $response->get_data()['code'], 'Response: ' . wp_json_encode( $response ) ); + $this->assertSame( $expected_status, $response->get_status(), 'Response: ' . wp_json_encode( $response ) ); + $this->assertSame( $expected_code, $response->get_data()['code'], 'Response: ' . wp_json_encode( $response ) ); $this->assertNull( OD_URL_Metrics_Post_Type::get_post( $params['slug'] ) ); $this->assertSame( 0, did_action( 'od_url_metric_stored' ) );