Skip to content

Commit 2183f23

Browse files
committed
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. Props czarate, westonruter, joefusco. Fixes #64890. git-svn-id: https://develop.svn.wordpress.org/trunk@62198 602fd350-edb4-49c9-b593-d223f7449a82
1 parent b5da8de commit 2183f23

File tree

2 files changed

+378
-17
lines changed

2 files changed

+378
-17
lines changed

src/wp-includes/collaboration/class-wp-http-polling-sync-server.php

Lines changed: 88 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,30 @@ class WP_HTTP_Polling_Sync_Server {
3737
*/
3838
const COMPACTION_THRESHOLD = 50;
3939

40+
/**
41+
* Maximum total size (in bytes) of the request body.
42+
*
43+
* @since 7.0.0
44+
* @var int
45+
*/
46+
const MAX_BODY_SIZE = 16 * MB_IN_BYTES;
47+
48+
/**
49+
* Maximum number of rooms allowed per request.
50+
*
51+
* @since 7.0.0
52+
* @var int
53+
*/
54+
const MAX_ROOMS_PER_REQUEST = 50;
55+
56+
/**
57+
* Maximum length of a single update data string.
58+
*
59+
* @since 7.0.0
60+
* @var int
61+
*/
62+
const MAX_UPDATE_DATA_SIZE = MB_IN_BYTES;
63+
4064
/**
4165
* Sync update type: compaction.
4266
*
@@ -96,8 +120,9 @@ public function register_routes(): void {
96120
$typed_update_args = array(
97121
'properties' => array(
98122
'data' => array(
99-
'type' => 'string',
100-
'required' => true,
123+
'type' => 'string',
124+
'required' => true,
125+
'maxLength' => self::MAX_UPDATE_DATA_SIZE,
101126
),
102127
'type' => array(
103128
'type' => 'string',
@@ -149,12 +174,14 @@ public function register_routes(): void {
149174
'methods' => array( WP_REST_Server::CREATABLE ),
150175
'callback' => array( $this, 'handle_request' ),
151176
'permission_callback' => array( $this, 'check_permissions' ),
177+
'validate_callback' => array( $this, 'validate_request' ),
152178
'args' => array(
153179
'rooms' => array(
154180
'items' => array(
155181
'properties' => $room_args,
156182
'type' => 'object',
157183
),
184+
'maxItems' => self::MAX_ROOMS_PER_REQUEST,
158185
'required' => true,
159186
'type' => 'array',
160187
),
@@ -223,6 +250,30 @@ public function check_permissions( WP_REST_Request $request ) {
223250
return true;
224251
}
225252

253+
/**
254+
* Validates that the request body does not exceed the maximum allowed size.
255+
*
256+
* Runs as the route-level validate_callback, after per-arg schema
257+
* validation has already passed.
258+
*
259+
* @since 7.0.0
260+
*
261+
* @param WP_REST_Request $request The REST request.
262+
* @return true|WP_Error True if valid, WP_Error if the body is too large.
263+
*/
264+
public function validate_request( WP_REST_Request $request ) {
265+
$body = $request->get_body();
266+
if ( is_string( $body ) && strlen( $body ) > self::MAX_BODY_SIZE ) {
267+
return new WP_Error(
268+
'rest_sync_body_too_large',
269+
__( 'Request body is too large.' ),
270+
array( 'status' => 413 )
271+
);
272+
}
273+
274+
return true;
275+
}
276+
226277
/**
227278
* Handles request: stores sync updates and awareness data, and returns
228279
* updates the client is missing.
@@ -278,24 +329,47 @@ public function handle_request( WP_REST_Request $request ) {
278329
*
279330
* @param string $entity_kind The entity kind, e.g. 'postType', 'taxonomy', 'root'.
280331
* @param string $entity_name The entity name, e.g. 'post', 'category', 'site'.
281-
* @param string|null $object_id The object ID / entity key for single entities, null for collections.
332+
* @param string|null $object_id The numeric object ID / entity key for single entities, null for collections.
282333
* @return bool True if user has permission, otherwise false.
283334
*/
284335
private function can_user_sync_entity_type( string $entity_kind, string $entity_name, ?string $object_id ): bool {
285-
// Handle single post type entities with a defined object ID.
286-
if ( 'postType' === $entity_kind && is_numeric( $object_id ) ) {
287-
return current_user_can( 'edit_post', (int) $object_id );
336+
if ( is_string( $object_id ) ) {
337+
if ( ! ctype_digit( $object_id ) ) {
338+
return false;
339+
}
340+
$object_id = (int) $object_id;
288341
}
289-
290-
// Handle single taxonomy term entities with a defined object ID.
291-
if ( 'taxonomy' === $entity_kind && is_numeric( $object_id ) ) {
292-
$taxonomy = get_taxonomy( $entity_name );
293-
return isset( $taxonomy->cap->assign_terms ) && current_user_can( $taxonomy->cap->assign_terms );
342+
if ( null !== $object_id && $object_id <= 0 ) {
343+
// Object ID must be numeric if provided.
344+
return false;
294345
}
295346

296-
// Handle single comment entities with a defined object ID.
297-
if ( 'root' === $entity_kind && 'comment' === $entity_name && is_numeric( $object_id ) ) {
298-
return current_user_can( 'edit_comment', (int) $object_id );
347+
// Validate permissions for the provided object ID.
348+
if ( is_int( $object_id ) ) {
349+
// Handle single post type entities with a defined object ID.
350+
if ( 'postType' === $entity_kind ) {
351+
if ( get_post_type( $object_id ) !== $entity_name ) {
352+
// Post is not of the specified post type.
353+
return false;
354+
}
355+
return current_user_can( 'edit_post', $object_id );
356+
}
357+
358+
// Handle single taxonomy term entities with a defined object ID.
359+
if ( 'taxonomy' === $entity_kind ) {
360+
$term_exists = term_exists( $object_id, $entity_name );
361+
if ( ! is_array( $term_exists ) || ! isset( $term_exists['term_id'] ) ) {
362+
// Either term doesn't exist OR term is not in specified taxonomy.
363+
return false;
364+
}
365+
366+
return current_user_can( 'edit_term', $object_id );
367+
}
368+
369+
// Handle single comment entities with a defined object ID.
370+
if ( 'root' === $entity_kind && 'comment' === $entity_name ) {
371+
return current_user_can( 'edit_comment', $object_id );
372+
}
299373
}
300374

301375
// All the remaining checks are for collections. If an object ID is provided,

0 commit comments

Comments
 (0)