Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0229c11
REST API: Add 'scaled' to sideload route image_size enum
adamsilverstein Feb 23, 2026
3832e25
Merge branch 'trunk' into add-scaled-to-sideload-route
adamsilverstein Feb 26, 2026
0dcab83
REST API: Update auto-generated JS fixture for sideload route.
adamsilverstein Feb 26, 2026
2fe2162
Tests: Add unit tests for scaled image sideloading via REST API.
adamsilverstein Feb 26, 2026
133ec0f
REST API: Validate get_attached_file() in sideload
adamsilverstein Feb 26, 2026
a868b57
Tests: Fix expected error code in sideload auth test
adamsilverstein Feb 26, 2026
e5baa99
fix ticket number for test annotations
adamsilverstein Mar 2, 2026
5405e38
Merge branch 'trunk' into add-scaled-to-sideload-route
adamsilverstein Mar 2, 2026
02a9794
Merge branch 'trunk' into add-scaled-to-sideload-route
adamsilverstein Mar 3, 2026
458d94c
Add value assertion for metadata file key
adamsilverstein Mar 3, 2026
978ac79
Extract image_size key into variable
adamsilverstein Mar 3, 2026
eeca10e
Add test for scaled filename numeric suffix on conflict
adamsilverstein Mar 3, 2026
ae029f1
Cast $number to int in regex for safety
adamsilverstein Mar 3, 2026
a484e07
Apply suggestion from @westonruter
adamsilverstein Mar 3, 2026
fbe7458
Apply suggestion from @westonruter
adamsilverstein Mar 3, 2026
b30edb6
Handle update_attached_file failure in sideload
adamsilverstein Mar 3, 2026
56c05e4
Use is_int check instead of empty for $number
adamsilverstein Mar 3, 2026
d558ceb
Validate scaled image before updating attached file
adamsilverstein Mar 3, 2026
851f14a
Improve regex grouping in filter_wp_unique_filename.
adamsilverstein Mar 3, 2026
6a95756
REST API: Add dimension validation to sideload endpoint.
adamsilverstein Mar 1, 2026
2cb2454
REST API: Refactor dimension validation in sideload endpoint.
adamsilverstein Mar 2, 2026
5474fcd
Update src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-c…
adamsilverstein Mar 3, 2026
394bec7
Merge branch 'trunk' into add-dimension-validation-to-sideload
adamsilverstein Mar 3, 2026
0cec0b0
revert unrelated
adamsilverstein Mar 3, 2026
2566f7c
revert dupicate change
adamsilverstein Mar 3, 2026
3a17ad8
Merge branch 'trunk' into add-dimension-validation-to-sideload
adamsilverstein Mar 4, 2026
5b3d79c
Merge branch 'trunk' into add-dimension-validation-to-sideload
adamsilverstein Mar 5, 2026
c57a975
REST API: Refactor sideload dimension validation per review.
adamsilverstein May 6, 2026
642209e
Merge remote-tracking branch 'origin/trunk' into add-dimension-valida…
adamsilverstein May 28, 2026
5634d1d
Add missing types for image sizes
westonruter May 28, 2026
c7ea9f6
REST API: Add tests for sideload dimension validation.
adamsilverstein May 29, 2026
5a46435
Media: Preserve image sub-size key order in wp_get_registered_image_s…
adamsilverstein Jun 9, 2026
b287aa0
REST API: Reject unreadable images on the sideload endpoint
adamsilverstein Jun 28, 2026
315e9c4
Apply suggestion from @adamsilverstein
adamsilverstein Jun 29, 2026
8756605
Apply suggestion from @adamsilverstein
adamsilverstein Jun 29, 2026
724b204
Merge trunk into add-dimension-validation-to-sideload
adamsilverstein Jul 1, 2026
0f05845
Exempt source-format originals from sideload dimension validation
adamsilverstein Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/wp-includes/media.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,17 @@
*
* @since 4.7.0
*
* @see add_image_size()
*
* @global array $_wp_additional_image_sizes
*
* @return array Additional images size data.
*
* @phpstan-return array<string, array{
* width: non-negative-int,
* height: non-negative-int,
* crop: array{ 'left'|'center'|'right', 'top'|'center'|'bottom' }|bool,
* }>
*/
function wp_get_additional_image_sizes() {
global $_wp_additional_image_sizes;
Expand Down Expand Up @@ -296,6 +304,10 @@ function image_downsize( $id, $size = 'medium' ) {
* @type string $0 The x crop position. Accepts 'left', 'center', or 'right'.
* @type string $1 The y crop position. Accepts 'top', 'center', or 'bottom'.
* }
*
* @phpstan-param non-negative-int $width
* @phpstan-param non-negative-int $height
* @phpstan-param array{ 'left'|'center'|'right', 'top'|'center'|'bottom' }|bool $crop
*/
function add_image_size( $name, $width = 0, $height = 0, $crop = false ) {
global $_wp_additional_image_sizes;
Expand Down Expand Up @@ -908,8 +920,14 @@ function get_intermediate_image_sizes() {
*
* @return array[] Associative array of arrays of image sub-size information,
* keyed by image size name.
*
* @phpstan-return array<string, array{
* width: non-negative-int,
* height: non-negative-int,
* crop: array{ 'left'|'center'|'right', 'top'|'center'|'bottom' }|bool,
* }>
*/
function wp_get_registered_image_subsizes() {
function wp_get_registered_image_subsizes(): array {
$additional_sizes = wp_get_additional_image_sizes();
$all_sizes = array();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2117,6 +2117,123 @@ public function sideload_item_permissions_check( $request ) {
return $this->edit_media_item_permissions_check( $request );
}

/**
* Validates that uploaded image dimensions are appropriate for the specified image size.
*
* @since 7.1.0
*
* @param int $width Uploaded image width.
* @param int $height Uploaded image height.
* @param string $image_size The target image size name.
* @param int $attachment_id The attachment ID.
* @return true|WP_Error True if valid, WP_Error if invalid.
*/
private function validate_image_dimensions( int $width, int $height, string $image_size, int $attachment_id ) {
// All image sizes require positive dimensions.
if ( $width <= 0 || $height <= 0 ) {
return new WP_Error(
'rest_upload_invalid_dimensions',
__( 'Uploaded image must have positive dimensions.' ),
array( 'status' => 400 )
);
}

// 'original' size: should match original attachment dimensions.
if ( 'original' === $image_size ) {
$metadata = wp_get_attachment_metadata( $attachment_id, true );
if ( is_array( $metadata ) && isset( $metadata['width'], $metadata['height'] ) ) {
$expected_width = (int) $metadata['width'];
$expected_height = (int) $metadata['height'];

if ( $width !== $expected_width || $height !== $expected_height ) {
return new WP_Error(
'rest_upload_dimension_mismatch',
sprintf(
/* translators: 1: Actual width, 2: actual height, 3: expected width, 4: expected height. */
__( 'Uploaded image dimensions (%1$dx%2$d) do not match original image dimensions (%3$dx%4$d).' ),
$width,
$height,
$expected_width,
$expected_height
),
array( 'status' => 400 )
);
}
}
return true;
}

// 'full' size (PDF thumbnails) and 'scaled': no further constraints.
if ( in_array( $image_size, array( 'full', 'scaled' ), true ) ) {
return true;
}

// Regular image sizes: validate against registered size constraints.
$registered_sizes = wp_get_registered_image_subsizes();

if ( ! isset( $registered_sizes[ $image_size ] ) ) {
return new WP_Error(
'rest_upload_unknown_size',
__( 'Unknown image size.' ),
array( 'status' => 400 )
);
}

$size_data = $registered_sizes[ $image_size ];
$max_width = (int) $size_data['width'];
$max_height = (int) $size_data['height'];

// Validate dimensions don't exceed the registered size maximums.
// Allow 1px tolerance for rounding differences.
$tolerance = 1;

if ( $this->dimension_exceeds_max( $width, $max_width, $tolerance ) ) {
return new WP_Error(
'rest_upload_dimension_mismatch',
sprintf(
/* translators: 1: Image size name, 2: maximum width, 3: actual width. */
__( 'Uploaded image width (%3$d) exceeds maximum for "%1$s" size (%2$d).' ),
$image_size,
$max_width,
$width
),
array( 'status' => 400 )
);
}

if ( $this->dimension_exceeds_max( $height, $max_height, $tolerance ) ) {
return new WP_Error(
'rest_upload_dimension_mismatch',
sprintf(
/* translators: 1: Image size name, 2: maximum height, 3: actual height. */
__( 'Uploaded image height (%3$d) exceeds maximum for "%1$s" size (%2$d).' ),
$image_size,
$max_height,
$height
),
array( 'status' => 400 )
);
}

return true;
}

/**
* Checks whether a dimension exceeds the maximum allowed value.
*
* A maximum of zero means the dimension is unconstrained.
*
* @since 7.1.0
*
* @param int $value The actual dimension in pixels.
* @param int $max The maximum allowed dimension in pixels. Zero means no constraint.
* @param int $tolerance Pixel tolerance allowed for rounding differences.
* @return bool True if the value exceeds the maximum plus tolerance.
*/
private function dimension_exceeds_max( int $value, int $max, int $tolerance ): bool {
return $max > 0 && $value > $max + $tolerance;
}

/**
* Side-loads a media file without creating a new attachment.
*
Expand Down Expand Up @@ -2194,8 +2311,49 @@ public function sideload_item( WP_REST_Request $request ) {
$type = $file['type'];
$path = $file['file'];

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

/*
* Validate raster sub-sizes before storing them. Source-format companion
* originals (e.g. a HEIC or JXL kept next to its JPEG derivative) are
* exempt: their dimensions are neither validated nor recorded, and the
* source format may not be readable by wp_getimagesize() at all.
*/
if ( self::IMAGE_SIZE_SOURCE_ORIGINAL !== $image_size ) {
/*
* Read the dimensions up front. A file whose dimensions cannot be
* read is corrupted or an unsupported format and must be rejected
* rather than silently stored with zero dimensions.
*/
$size = wp_getimagesize( $path );

if ( ! $size ) {
// Clean up the uploaded file.
wp_delete_file( $path );
return new WP_Error(
'rest_upload_invalid_image',
__( 'Could not read image dimensions. The file may be corrupted or an unsupported format.' ),
array( 'status' => 400 )
);
}

/*
* Validate the dimensions match the expected size. An array
* $image_size represents multiple registered sizes sharing a single
* file; those are handled by the per-size branch below, so only
* scalar sizes are validated here.
*/
if ( ! is_array( $image_size ) ) {
$validation = $this->validate_image_dimensions( $size[0], $size[1], $image_size, $attachment_id );
if ( is_wp_error( $validation ) ) {
// Clean up the uploaded file.
wp_delete_file( $path );
return $validation;
}
}
}

// Build sub-size data to return to the client.
// The client accumulates these and sends them all to the finalize
// endpoint, which writes the metadata in a single operation. This
Expand Down
Loading
Loading