Skip to content

Commit 27deb59

Browse files
REST API: Add dimension validation to sideload endpoint.
Validate uploaded image dimensions in the `wp/v2/media/<id>/sideload` endpoint before metadata is written, ensuring sideloaded files match the requested `image_size`. A new private `validate_image_dimensions()` helper on `WP_REST_Attachments_Controller` enforces: - The `original` size must match the original attachment dimensions exactly. - The `full` and `scaled` sizes require positive dimensions only. - Regular registered sizes must not exceed the registered sub-size maximums, with a 1px tolerance for rounding differences. Follow-up to [62428]. Props apermo, westonruter. Fixes #64798. git-svn-id: https://develop.svn.wordpress.org/trunk@62619 602fd350-edb4-49c9-b593-d223f7449a82
1 parent a42f847 commit 27deb59

3 files changed

Lines changed: 353 additions & 2 deletions

File tree

src/wp-includes/media.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,17 @@
1616
*
1717
* @since 4.7.0
1818
*
19+
* @see add_image_size()
20+
*
1921
* @global array $_wp_additional_image_sizes
2022
*
2123
* @return array Additional images size data.
24+
*
25+
* @phpstan-return array<string, array{
26+
* width: non-negative-int,
27+
* height: non-negative-int,
28+
* crop: array{ 'left'|'center'|'right', 'top'|'center'|'bottom' }|bool,
29+
* }>
2230
*/
2331
function wp_get_additional_image_sizes() {
2432
global $_wp_additional_image_sizes;
@@ -296,6 +304,10 @@ function image_downsize( $id, $size = 'medium' ) {
296304
* @type string $0 The x crop position. Accepts 'left', 'center', or 'right'.
297305
* @type string $1 The y crop position. Accepts 'top', 'center', or 'bottom'.
298306
* }
307+
*
308+
* @phpstan-param non-negative-int $width
309+
* @phpstan-param non-negative-int $height
310+
* @phpstan-param array{ 'left'|'center'|'right', 'top'|'center'|'bottom' }|bool $crop
299311
*/
300312
function add_image_size( $name, $width = 0, $height = 0, $crop = false ) {
301313
global $_wp_additional_image_sizes;
@@ -908,8 +920,14 @@ function get_intermediate_image_sizes() {
908920
*
909921
* @return array[] Associative array of arrays of image sub-size information,
910922
* keyed by image size name.
923+
*
924+
* @phpstan-return array<string, array{
925+
* width: non-negative-int,
926+
* height: non-negative-int,
927+
* crop: array{ 'left'|'center'|'right', 'top'|'center'|'bottom' }|bool,
928+
* }>
911929
*/
912-
function wp_get_registered_image_subsizes() {
930+
function wp_get_registered_image_subsizes(): array {
913931
$additional_sizes = wp_get_additional_image_sizes();
914932
$all_sizes = array();
915933

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

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2166,6 +2166,123 @@ public function sideload_item_permissions_check( $request ) {
21662166
return $this->edit_media_item_permissions_check( $request );
21672167
}
21682168

2169+
/**
2170+
* Validates that uploaded image dimensions are appropriate for the specified image size.
2171+
*
2172+
* @since 7.1.0
2173+
*
2174+
* @param int $width Uploaded image width.
2175+
* @param int $height Uploaded image height.
2176+
* @param string $image_size The target image size name.
2177+
* @param int $attachment_id The attachment ID.
2178+
* @return true|WP_Error True if valid, WP_Error if invalid.
2179+
*/
2180+
private function validate_image_dimensions( int $width, int $height, string $image_size, int $attachment_id ) {
2181+
// All image sizes require positive dimensions.
2182+
if ( $width <= 0 || $height <= 0 ) {
2183+
return new WP_Error(
2184+
'rest_upload_invalid_dimensions',
2185+
__( 'Uploaded image must have positive dimensions.' ),
2186+
array( 'status' => 400 )
2187+
);
2188+
}
2189+
2190+
// 'original' size: should match original attachment dimensions.
2191+
if ( 'original' === $image_size ) {
2192+
$metadata = wp_get_attachment_metadata( $attachment_id, true );
2193+
if ( is_array( $metadata ) && isset( $metadata['width'], $metadata['height'] ) ) {
2194+
$expected_width = (int) $metadata['width'];
2195+
$expected_height = (int) $metadata['height'];
2196+
2197+
if ( $width !== $expected_width || $height !== $expected_height ) {
2198+
return new WP_Error(
2199+
'rest_upload_dimension_mismatch',
2200+
sprintf(
2201+
/* translators: 1: Actual width, 2: actual height, 3: expected width, 4: expected height. */
2202+
__( 'Uploaded image dimensions (%1$dx%2$d) do not match original image dimensions (%3$dx%4$d).' ),
2203+
$width,
2204+
$height,
2205+
$expected_width,
2206+
$expected_height
2207+
),
2208+
array( 'status' => 400 )
2209+
);
2210+
}
2211+
}
2212+
return true;
2213+
}
2214+
2215+
// 'full' size (PDF thumbnails) and 'scaled': no further constraints.
2216+
if ( in_array( $image_size, array( 'full', 'scaled' ), true ) ) {
2217+
return true;
2218+
}
2219+
2220+
// Regular image sizes: validate against registered size constraints.
2221+
$registered_sizes = wp_get_registered_image_subsizes();
2222+
2223+
if ( ! isset( $registered_sizes[ $image_size ] ) ) {
2224+
return new WP_Error(
2225+
'rest_upload_unknown_size',
2226+
__( 'Unknown image size.' ),
2227+
array( 'status' => 400 )
2228+
);
2229+
}
2230+
2231+
$size_data = $registered_sizes[ $image_size ];
2232+
$max_width = (int) $size_data['width'];
2233+
$max_height = (int) $size_data['height'];
2234+
2235+
// Validate dimensions don't exceed the registered size maximums.
2236+
// Allow 1px tolerance for rounding differences.
2237+
$tolerance = 1;
2238+
2239+
if ( $this->dimension_exceeds_max( $width, $max_width, $tolerance ) ) {
2240+
return new WP_Error(
2241+
'rest_upload_dimension_mismatch',
2242+
sprintf(
2243+
/* translators: 1: Image size name, 2: maximum width, 3: actual width. */
2244+
__( 'Uploaded image width (%3$d) exceeds maximum for "%1$s" size (%2$d).' ),
2245+
$image_size,
2246+
$max_width,
2247+
$width
2248+
),
2249+
array( 'status' => 400 )
2250+
);
2251+
}
2252+
2253+
if ( $this->dimension_exceeds_max( $height, $max_height, $tolerance ) ) {
2254+
return new WP_Error(
2255+
'rest_upload_dimension_mismatch',
2256+
sprintf(
2257+
/* translators: 1: Image size name, 2: maximum height, 3: actual height. */
2258+
__( 'Uploaded image height (%3$d) exceeds maximum for "%1$s" size (%2$d).' ),
2259+
$image_size,
2260+
$max_height,
2261+
$height
2262+
),
2263+
array( 'status' => 400 )
2264+
);
2265+
}
2266+
2267+
return true;
2268+
}
2269+
2270+
/**
2271+
* Checks whether a dimension exceeds the maximum allowed value.
2272+
*
2273+
* A maximum of zero means the dimension is unconstrained.
2274+
*
2275+
* @since 7.1.0
2276+
*
2277+
* @param int $value The actual dimension in pixels.
2278+
* @param int $max The maximum allowed dimension in pixels. Zero means no constraint.
2279+
* @param int $tolerance Pixel tolerance allowed for rounding differences.
2280+
* @return bool True if the value exceeds the maximum plus tolerance.
2281+
*/
2282+
private function dimension_exceeds_max( int $value, int $max, int $tolerance ): bool {
2283+
return $max > 0 && $value > $max + $tolerance;
2284+
}
2285+
21692286
/**
21702287
* Side-loads a media file without creating a new attachment.
21712288
*
@@ -2243,8 +2360,49 @@ public function sideload_item( WP_REST_Request $request ) {
22432360
$type = $file['type'];
22442361
$path = $file['file'];
22452362

2363+
/** @var non-empty-string $image_size */
22462364
$image_size = $request['image_size'];
22472365

2366+
/*
2367+
* Validate raster sub-sizes before storing them. Source-format companion
2368+
* originals (e.g. a HEIC or JXL kept next to its JPEG derivative) are
2369+
* exempt: their dimensions are neither validated nor recorded, and the
2370+
* source format may not be readable by wp_getimagesize() at all.
2371+
*/
2372+
if ( self::IMAGE_SIZE_SOURCE_ORIGINAL !== $image_size ) {
2373+
/*
2374+
* Read the dimensions up front. A file whose dimensions cannot be
2375+
* read is corrupted or an unsupported format and must be rejected
2376+
* rather than silently stored with zero dimensions.
2377+
*/
2378+
$size = wp_getimagesize( $path );
2379+
2380+
if ( ! $size ) {
2381+
// Clean up the uploaded file.
2382+
wp_delete_file( $path );
2383+
return new WP_Error(
2384+
'rest_upload_invalid_image',
2385+
__( 'Could not read image dimensions. The file may be corrupted or an unsupported format.' ),
2386+
array( 'status' => 400 )
2387+
);
2388+
}
2389+
2390+
/*
2391+
* Validate the dimensions match the expected size. An array
2392+
* $image_size represents multiple registered sizes sharing a single
2393+
* file; those are handled by the per-size branch below, so only
2394+
* scalar sizes are validated here.
2395+
*/
2396+
if ( ! is_array( $image_size ) ) {
2397+
$validation = $this->validate_image_dimensions( $size[0], $size[1], $image_size, $attachment_id );
2398+
if ( is_wp_error( $validation ) ) {
2399+
// Clean up the uploaded file.
2400+
wp_delete_file( $path );
2401+
return $validation;
2402+
}
2403+
}
2404+
}
2405+
22482406
// Build sub-size data to return to the client.
22492407
// The client accumulates these and sends them all to the finalize
22502408
// endpoint, which writes the metadata in a single operation. This

0 commit comments

Comments
 (0)