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/tests/phpunit/tests/rest-api/rest-attachments-controller.php

    r62617 r62619  
    38253825
    38263826        /**
     3827         * Tests that sideloading rejects an image whose dimensions exceed the
     3828         * registered maximum for the target image size.
     3829         *
     3830         * @ticket 64798
     3831         * @covers WP_REST_Attachments_Controller::sideload_item
     3832         * @covers WP_REST_Attachments_Controller::validate_image_dimensions
     3833         * @requires function imagejpeg
     3834         */
     3835        public function test_sideload_item_rejects_oversized_dimensions() {
     3836                $this->enable_client_side_media_processing();
     3837
     3838                wp_set_current_user( self::$author_id );
     3839
     3840                // Create an attachment from canola.jpg (640x480).
     3841                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     3842                $request->set_header( 'Content-Type', 'image/jpeg' );
     3843                $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
     3844                $request->set_body( file_get_contents( self::$test_file ) );
     3845                $response      = rest_get_server()->dispatch( $request );
     3846                $attachment_id = $response->get_data()['id'];
     3847
     3848                // Sideload the 640x480 image claiming it is a thumbnail (150x150 max).
     3849                $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );
     3850                $request->set_header( 'Content-Type', 'image/jpeg' );
     3851                $request->set_header( 'Content-Disposition', 'attachment; filename=canola-150x150.jpg' );
     3852                $request->set_param( 'image_size', 'thumbnail' );
     3853                $request->set_body( file_get_contents( self::$test_file ) );
     3854                $response = rest_get_server()->dispatch( $request );
     3855
     3856                $this->assertSame( 400, $response->get_status(), 'Oversized sideload should be rejected.' );
     3857                $this->assertSame( 'rest_upload_dimension_mismatch', $response->get_data()['code'] );
     3858        }
     3859
     3860        /**
     3861         * Tests that sideloading accepts an image whose dimensions fit within the
     3862         * registered maximum for the target image size.
     3863         *
     3864         * @ticket 64798
     3865         * @covers WP_REST_Attachments_Controller::sideload_item
     3866         * @covers WP_REST_Attachments_Controller::validate_image_dimensions
     3867         * @requires function imagejpeg
     3868         */
     3869        public function test_sideload_item_accepts_valid_dimensions() {
     3870                $this->enable_client_side_media_processing();
     3871
     3872                wp_set_current_user( self::$author_id );
     3873
     3874                // Create an attachment from canola.jpg (640x480).
     3875                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     3876                $request->set_header( 'Content-Type', 'image/jpeg' );
     3877                $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
     3878                $request->set_body( file_get_contents( self::$test_file ) );
     3879                $response      = rest_get_server()->dispatch( $request );
     3880                $attachment_id = $response->get_data()['id'];
     3881
     3882                // test-image.jpg is 50x50, well within the thumbnail maximum (150x150).
     3883                $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );
     3884                $request->set_header( 'Content-Type', 'image/jpeg' );
     3885                $request->set_header( 'Content-Disposition', 'attachment; filename=test-thumbnail.jpg' );
     3886                $request->set_param( 'image_size', 'thumbnail' );
     3887                $request->set_body( file_get_contents( DIR_TESTDATA . '/images/test-image.jpg' ) );
     3888                $response = rest_get_server()->dispatch( $request );
     3889
     3890                $this->assertSame( 200, $response->get_status(), 'Valid thumbnail sideload should succeed.' );
     3891        }
     3892
     3893        /**
     3894         * Tests that sideloading the 'original' size rejects an image whose
     3895         * dimensions do not match the original attachment dimensions.
     3896         *
     3897         * @ticket 64798
     3898         * @covers WP_REST_Attachments_Controller::sideload_item
     3899         * @covers WP_REST_Attachments_Controller::validate_image_dimensions
     3900         * @requires function imagejpeg
     3901         */
     3902        public function test_sideload_item_rejects_original_dimension_mismatch() {
     3903                $this->enable_client_side_media_processing();
     3904
     3905                wp_set_current_user( self::$author_id );
     3906
     3907                // Create an attachment from canola.jpg (640x480).
     3908                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     3909                $request->set_header( 'Content-Type', 'image/jpeg' );
     3910                $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
     3911                $request->set_body( file_get_contents( self::$test_file ) );
     3912                $response      = rest_get_server()->dispatch( $request );
     3913                $attachment_id = $response->get_data()['id'];
     3914
     3915                // Sideload a 50x50 image as the original; it does not match 640x480.
     3916                $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );
     3917                $request->set_header( 'Content-Type', 'image/jpeg' );
     3918                $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
     3919                $request->set_param( 'image_size', 'original' );
     3920                $request->set_body( file_get_contents( DIR_TESTDATA . '/images/test-image.jpg' ) );
     3921                $response = rest_get_server()->dispatch( $request );
     3922
     3923                $this->assertSame( 400, $response->get_status(), 'Mismatched original sideload should be rejected.' );
     3924                $this->assertSame( 'rest_upload_dimension_mismatch', $response->get_data()['code'] );
     3925        }
     3926
     3927        /**
     3928         * Tests that sideloading the 'original' size accepts an image whose
     3929         * dimensions match the original attachment dimensions.
     3930         *
     3931         * @ticket 64798
     3932         * @covers WP_REST_Attachments_Controller::sideload_item
     3933         * @covers WP_REST_Attachments_Controller::validate_image_dimensions
     3934         * @requires function imagejpeg
     3935         */
     3936        public function test_sideload_item_accepts_matching_original_dimensions() {
     3937                $this->enable_client_side_media_processing();
     3938
     3939                wp_set_current_user( self::$author_id );
     3940
     3941                // Create an attachment from canola.jpg (640x480).
     3942                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     3943                $request->set_header( 'Content-Type', 'image/jpeg' );
     3944                $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
     3945                $request->set_body( file_get_contents( self::$test_file ) );
     3946                $response      = rest_get_server()->dispatch( $request );
     3947                $attachment_id = $response->get_data()['id'];
     3948
     3949                // Sideload the same 640x480 image as the original; dimensions match.
     3950                $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );
     3951                $request->set_header( 'Content-Type', 'image/jpeg' );
     3952                $request->set_header( 'Content-Disposition', 'attachment; filename=canola-original.jpg' );
     3953                $request->set_param( 'image_size', 'original' );
     3954                $request->set_body( file_get_contents( self::$test_file ) );
     3955                $response = rest_get_server()->dispatch( $request );
     3956
     3957                $this->assertSame( 200, $response->get_status(), 'Matching original sideload should succeed.' );
     3958        }
     3959
     3960        /**
     3961         * Tests that sideloading a file whose dimensions cannot be read is rejected
     3962         * rather than stored with zero dimensions.
     3963         *
     3964         * The body is a JFIF header with no frame data: its magic bytes identify it
     3965         * as a JPEG so the upload itself succeeds, but wp_getimagesize() cannot
     3966         * determine dimensions, which is the corrupted/unsupported-format case.
     3967         *
     3968         * @ticket 64798
     3969         * @covers WP_REST_Attachments_Controller::sideload_item
     3970         */
     3971        public function test_sideload_item_rejects_unreadable_image() {
     3972                $this->enable_client_side_media_processing();
     3973
     3974                wp_set_current_user( self::$author_id );
     3975
     3976                // Create an attachment from canola.jpg (640x480).
     3977                $request = new WP_REST_Request( 'POST', '/wp/v2/media' );
     3978                $request->set_header( 'Content-Type', 'image/jpeg' );
     3979                $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );
     3980                $request->set_body( file_get_contents( self::$test_file ) );
     3981                $response      = rest_get_server()->dispatch( $request );
     3982                $attachment_id = $response->get_data()['id'];
     3983
     3984                // A JPEG SOI + JFIF APP0 marker followed immediately by EOI: valid magic
     3985                // bytes, but no SOF marker, so wp_getimagesize() returns false.
     3986                $unreadable = "\xFF\xD8\xFF\xE0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xFF\xD9";
     3987
     3988                $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );
     3989                $request->set_header( 'Content-Type', 'image/jpeg' );
     3990                $request->set_header( 'Content-Disposition', 'attachment; filename=canola-thumbnail.jpg' );
     3991                $request->set_param( 'image_size', 'thumbnail' );
     3992                $request->set_body( $unreadable );
     3993                $response = rest_get_server()->dispatch( $request );
     3994
     3995                $this->assertSame( 400, $response->get_status(), 'Unreadable image sideload should be rejected.' );
     3996                $this->assertSame( 'rest_upload_invalid_image', $response->get_data()['code'] );
     3997        }
     3998
     3999        /**
    38274000         * Tests that the finalize endpoint triggers wp_generate_attachment_metadata.
    38284001         *
    … …  
    39514124
    39524125                // Sideload a thumbnail sub-size; the response carries its metadata.
     4126                // test-image.jpg is 50x50, within the registered thumbnail maximum
     4127                // (150x150), so it passes sideload dimension validation.
    39534128                $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );
    39544129                $request->set_header( 'Content-Type', 'image/jpeg' );
    39554130                $request->set_header( 'Content-Disposition', 'attachment; filename=canola-thumb.jpg' );
    39564131                $request->set_param( 'image_size', 'thumbnail' );
    3957                 $request->set_body( (string) file_get_contents( self::$test_file ) );
     4132                $request->set_body( (string) file_get_contents( DIR_TESTDATA . '/images/test-image.jpg' ) );
    39584133                $response = rest_get_server()->dispatch( $request );
    39594134
Note: See TracChangeset for help on using the changeset viewer.