Skip to content

Commit 6a95756

Browse files
REST API: Add dimension validation to sideload endpoint.
Validates uploaded image dimensions against expected size constraints in the wp/v2/media/<id>/sideload endpoint. This prevents users from uploading incorrectly-sized images for a specified image size. Validation rules: - 'original' size: must match original attachment dimensions exactly. - 'full' and 'scaled' sizes: requires positive dimensions only. - Regular sizes: dimensions must not exceed registered size maximums (with 1px tolerance for rounding differences). Also adds two new test cases for dimension validation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 851f14a commit 6a95756

2 files changed

Lines changed: 178 additions & 2 deletions

File tree

src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1968,6 +1968,114 @@ public function sideload_item_permissions_check( $request ) {
19681968
return $this->edit_media_item_permissions_check( $request );
19691969
}
19701970

1971+
/**
1972+
* Validates that uploaded image dimensions are appropriate for the specified image size.
1973+
*
1974+
* @since 7.0.0
1975+
*
1976+
* @param int $width Uploaded image width.
1977+
* @param int $height Uploaded image height.
1978+
* @param string $image_size The target image size name.
1979+
* @param int $attachment_id The attachment ID.
1980+
* @return true|WP_Error True if valid, WP_Error if invalid.
1981+
*/
1982+
private function validate_image_dimensions( int $width, int $height, string $image_size, int $attachment_id ) {
1983+
// 'original' size: should match original attachment dimensions.
1984+
if ( 'original' === $image_size ) {
1985+
$metadata = wp_get_attachment_metadata( $attachment_id, true );
1986+
if ( is_array( $metadata ) && isset( $metadata['width'], $metadata['height'] ) ) {
1987+
$expected_width = (int) $metadata['width'];
1988+
$expected_height = (int) $metadata['height'];
1989+
1990+
if ( $width !== $expected_width || $height !== $expected_height ) {
1991+
return new WP_Error(
1992+
'rest_upload_dimension_mismatch',
1993+
sprintf(
1994+
/* translators: 1: Expected width, 2: expected height, 3: actual width, 4: actual height. */
1995+
__( 'Uploaded image dimensions (%3$dx%4$d) do not match original image dimensions (%1$dx%2$d).' ),
1996+
$expected_width,
1997+
$expected_height,
1998+
$width,
1999+
$height
2000+
),
2001+
array( 'status' => 400 )
2002+
);
2003+
}
2004+
}
2005+
return true;
2006+
}
2007+
2008+
// 'full' size (PDF thumbnails) and 'scaled': dimensions must be positive.
2009+
if ( 'full' === $image_size || 'scaled' === $image_size ) {
2010+
if ( $width <= 0 || $height <= 0 ) {
2011+
return new WP_Error(
2012+
'rest_upload_invalid_dimensions',
2013+
__( 'Uploaded image must have positive dimensions.' ),
2014+
array( 'status' => 400 )
2015+
);
2016+
}
2017+
return true;
2018+
}
2019+
2020+
// Regular image sizes: validate against registered size constraints.
2021+
$registered_sizes = wp_get_registered_image_subsizes();
2022+
2023+
if ( ! isset( $registered_sizes[ $image_size ] ) ) {
2024+
return new WP_Error(
2025+
'rest_upload_unknown_size',
2026+
__( 'Unknown image size.' ),
2027+
array( 'status' => 400 )
2028+
);
2029+
}
2030+
2031+
$size_data = $registered_sizes[ $image_size ];
2032+
$max_width = (int) $size_data['width'];
2033+
$max_height = (int) $size_data['height'];
2034+
2035+
// Dimensions must be positive.
2036+
if ( $width <= 0 || $height <= 0 ) {
2037+
return new WP_Error(
2038+
'rest_upload_invalid_dimensions',
2039+
__( 'Uploaded image must have positive dimensions.' ),
2040+
array( 'status' => 400 )
2041+
);
2042+
}
2043+
2044+
// Validate dimensions don't exceed the registered size maximums.
2045+
// Allow 1px tolerance for rounding differences.
2046+
$tolerance = 1;
2047+
2048+
if ( $max_width > 0 && $width > $max_width + $tolerance ) {
2049+
return new WP_Error(
2050+
'rest_upload_dimension_mismatch',
2051+
sprintf(
2052+
/* translators: 1: Image size name, 2: maximum width, 3: actual width. */
2053+
__( 'Uploaded image width (%3$d) exceeds maximum for "%1$s" size (%2$d).' ),
2054+
$image_size,
2055+
$max_width,
2056+
$width
2057+
),
2058+
array( 'status' => 400 )
2059+
);
2060+
}
2061+
2062+
if ( $max_height > 0 && $height > $max_height + $tolerance ) {
2063+
return new WP_Error(
2064+
'rest_upload_dimension_mismatch',
2065+
sprintf(
2066+
/* translators: 1: Image size name, 2: maximum height, 3: actual height. */
2067+
__( 'Uploaded image height (%3$d) exceeds maximum for "%1$s" size (%2$d).' ),
2068+
$image_size,
2069+
$max_height,
2070+
$height
2071+
),
2072+
array( 'status' => 400 )
2073+
);
2074+
}
2075+
2076+
return true;
2077+
}
2078+
19712079
/**
19722080
* Side-loads a media file without creating a new attachment.
19732081
*
@@ -2047,6 +2155,18 @@ public function sideload_item( WP_REST_Request $request ) {
20472155

20482156
$image_size = $request['image_size'];
20492157

2158+
$size = wp_getimagesize( $path );
2159+
2160+
// Validate dimensions match expected size.
2161+
if ( $size ) {
2162+
$validation = $this->validate_image_dimensions( $size[0], $size[1], $image_size, $attachment_id );
2163+
if ( is_wp_error( $validation ) ) {
2164+
// Clean up the uploaded file.
2165+
wp_delete_file( $path );
2166+
return $validation;
2167+
}
2168+
}
2169+
20502170
$metadata = wp_get_attachment_metadata( $attachment_id, true );
20512171

20522172
if ( ! $metadata ) {
@@ -2100,8 +2220,6 @@ public function sideload_item( WP_REST_Request $request ) {
21002220
} else {
21012221
$metadata['sizes'] = $metadata['sizes'] ?? array();
21022222

2103-
$size = wp_getimagesize( $path );
2104-
21052223
$metadata['sizes'][ $image_size ] = array(
21062224
'width' => $size ? $size[0] : 0,
21072225
'height' => $size ? $size[1] : 0,

tests/phpunit/tests/rest-api/rest-attachments-controller.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3343,4 +3343,62 @@ public function test_sideload_scaled_unique_filename_conflict() {
33433343
$basename = wp_basename( $new_file );
33443344
$this->assertMatchesRegularExpression( '/canola-scaled-\d+\.jpg$/', $basename, 'Scaled filename should have numeric suffix when file conflicts with a different attachment.' );
33453345
}
3346+
3347+
/**
3348+
* Tests that sideloading an oversized image for a registered size is rejected.
3349+
*
3350+
* @ticket 63
3351+
* @requires function imagejpeg
3352+
*/
3353+
public function test_sideload_item_rejects_oversized_dimensions() {
3354+
wp_set_current_user( self::$author_id );
3355+
3356+
// Create an attachment.
3357+
$request = new WP_REST_Request( 'POST', '/wp/v2/media' );
3358+
$request->set_header( 'Content-Type', 'image/jpeg' );
3359+
$request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
3360+
$request->set_body( file_get_contents( self::$test_file ) );
3361+
$response = rest_get_server()->dispatch( $request );
3362+
$attachment_id = $response->get_data()['id'];
3363+
3364+
// canola.jpg is 640x480, which exceeds the default thumbnail size (150x150).
3365+
$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );
3366+
$request->set_header( 'Content-Type', 'image/jpeg' );
3367+
$request->set_header( 'Content-Disposition', 'attachment; filename=canola-150x150.jpg' );
3368+
$request->set_param( 'image_size', 'thumbnail' );
3369+
$request->set_body( file_get_contents( self::$test_file ) );
3370+
$response = rest_get_server()->dispatch( $request );
3371+
3372+
$this->assertSame( 400, $response->get_status(), 'Oversized image should be rejected.' );
3373+
$this->assertSame( 'rest_upload_dimension_mismatch', $response->get_data()['code'], 'Error code should be rest_upload_dimension_mismatch.' );
3374+
}
3375+
3376+
/**
3377+
* Tests that sideloading a correctly sized image for a registered size succeeds.
3378+
*
3379+
* @ticket 63
3380+
* @requires function imagejpeg
3381+
*/
3382+
public function test_sideload_item_accepts_valid_dimensions() {
3383+
wp_set_current_user( self::$author_id );
3384+
3385+
// Create an attachment.
3386+
$request = new WP_REST_Request( 'POST', '/wp/v2/media' );
3387+
$request->set_header( 'Content-Type', 'image/jpeg' );
3388+
$request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
3389+
$request->set_body( file_get_contents( self::$test_file ) );
3390+
$response = rest_get_server()->dispatch( $request );
3391+
$attachment_id = $response->get_data()['id'];
3392+
3393+
// test-image.jpg is 50x50, which fits within the default thumbnail size (150x150).
3394+
$test_image = DIR_TESTDATA . '/images/test-image.jpg';
3395+
$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );
3396+
$request->set_header( 'Content-Type', 'image/jpeg' );
3397+
$request->set_header( 'Content-Disposition', 'attachment; filename=canola-50x50.jpg' );
3398+
$request->set_param( 'image_size', 'thumbnail' );
3399+
$request->set_body( file_get_contents( $test_image ) );
3400+
$response = rest_get_server()->dispatch( $request );
3401+
3402+
$this->assertSame( 200, $response->get_status(), 'Valid-sized image should be accepted.' );
3403+
}
33463404
}

0 commit comments

Comments
 (0)