Make WordPress Core


Ignore:
Timestamp:
07/01/2026 09:32:34 PM (3 months ago)
Author:
adamsilverstein
Message:

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.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php

    r62617 r62619  
    21682168
    21692169        /**
     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
     2286        /**
    21702287         * Side-loads a media file without creating a new attachment.
    21712288         *
    … …  
    22442361                $path = $file['file'];
    22452362
     2363                /** @var non-empty-string $image_size */
    22462364                $image_size = $request['image_size'];
     2365
     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                }
    22472405
    22482406                // Build sub-size data to return to the client.
Note: See TracChangeset for help on using the changeset viewer.