Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -630,12 +630,41 @@ protected function create_item_from_url( $request ) {
);
}

/*
* Cap the download at the same size the site would accept as a direct
* upload. check_upload_size() only applies on multisite, so without a
* ceiling here a single site has no limit at all on this path: the
* `upload_max_filesize` and `post_max_size` directives bound a request
* body, not a fetch the server makes itself.
*
* When `wp_max_upload_size` returns 0, no ceiling is applied.
*/
$max_size = (int) wp_max_upload_size();

/*
* Download the remote file with WordPress's HTTP API, which validates
* the host and blocks requests to private or local addresses. This is
* the same primitive core's media_sideload_image() relies on.
*
* `limit_response_size` stops the transfer once the limit is passed,
* so an oversized remote file is never written to disk in full. One
* byte over the ceiling is enough to fail the size check below.
*/
$limit_response_size = static function ( $args ) use ( $max_size ) {
$args['limit_response_size'] = $max_size + 1;
return $args;
};
Comment thread
adamsilverstein marked this conversation as resolved.

if ( $max_size > 0 ) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here is where we skip setting the limit.

add_filter( 'http_request_args', $limit_response_size );
}

$tmp_file = download_url( $url );

if ( $max_size > 0 ) {
remove_filter( 'http_request_args', $limit_response_size );
}

if ( is_wp_error( $tmp_file ) ) {
return $tmp_file;
}
Expand All @@ -653,6 +682,19 @@ protected function create_item_from_url( $request ) {
return $size_check;
}

if ( $max_size > 0 && wp_filesize( $tmp_file ) > $max_size ) {
if ( file_exists( $tmp_file ) ) {
wp_delete_file( $tmp_file );
}

return new WP_Error(
'rest_upload_file_too_big',
/* translators: %s: Maximum allowed file size in kilobytes. */
sprintf( __( 'This file is too big. Files must be less than %s KB in size.' ), number_format( $max_size / KB_IN_BYTES ) ),
array( 'status' => 400 )
);
}

$attachment_id = media_handle_sideload( $file_array, $post_id );

if ( is_wp_error( $attachment_id ) ) {
Expand Down
92 changes: 92 additions & 0 deletions tests/phpunit/tests/rest-api/rest-attachments-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -5563,6 +5563,98 @@ public function test_create_item_from_url_exceeds_multisite_site_upload_space()
$this->assertErrorResponse( 'rest_upload_limited_space', $response, 400 );
}

/**
* Verifies that the URL sideload path enforces the site's maximum upload
* size on single site as well as multisite.
*
* check_upload_size() returns early when ! is_multisite(), so before this
* check a single site had no ceiling at all on this path.
*
* @ticket 65517
*
* @covers WP_REST_Attachments_Controller::create_item_from_url
*/
public function test_create_item_from_url_exceeds_max_upload_size() {
$this->enable_client_side_media_processing();

wp_set_current_user( self::$superadmin_id );

// The fixture the download is mocked with is comfortably larger than this.
add_filter( 'upload_size_limit', array( $this, 'filter_small_upload_size_limit' ), 20 );
add_filter( 'pre_http_request', array( $this, 'mock_image_download' ), 10, 3 );

$request = new WP_REST_Request( 'POST', '/wp/v2/media' );
$request->set_param( 'url', 'https://example.com/too-big.jpg' );
$request->set_param( 'generate_sub_sizes', false );

$response = rest_get_server()->dispatch( $request );

remove_filter( 'pre_http_request', array( $this, 'mock_image_download' ), 10 );

$this->assertErrorResponse( 'rest_upload_file_too_big', $response, 400 );
}

/**
* Verifies that the download itself is bounded, so an oversized remote file
* is not written to disk in full before the size check rejects it.
*
* @ticket 65517
*
* @covers WP_REST_Attachments_Controller::create_item_from_url
*/
public function test_create_item_from_url_limits_the_download_size() {
$this->enable_client_side_media_processing();

wp_set_current_user( self::$superadmin_id );

$request_args = null;

$capture_args = static function ( $response, $args, $url ) use ( &$request_args ) {
$request_args = $args;

if ( ! empty( $args['filename'] ) ) {
copy( DIR_TESTDATA . '/images/canola.jpg', $args['filename'] );
}

return array(
'response' => array(
'code' => 200,
'message' => 'OK',
),
'headers' => array(),
'cookies' => array(),
'body' => '',
);
};

add_filter( 'pre_http_request', $capture_args, 10, 3 );

$request = new WP_REST_Request( 'POST', '/wp/v2/media' );
$request->set_param( 'url', 'https://example.com/photo.jpg' );
$request->set_param( 'generate_sub_sizes', false );

rest_get_server()->dispatch( $request );

remove_filter( 'pre_http_request', $capture_args, 10 );

$this->assertIsArray( $request_args, 'The download request should have been made.' );
$this->assertSame(
(int) wp_max_upload_size() + 1,
$request_args['limit_response_size'],
'The download should be capped one byte past the maximum upload size.'
);
}

/**
* Filters the maximum upload size down to a value smaller than the image
* fixture used to mock the download.
*
* @return int A deliberately small upload size limit, in bytes.
*/
public function filter_small_upload_size_limit() {
return 1024;
}

/**
* Verifies that a URL with no usable path bails with a 400 before any
* download is attempted, rather than handing an empty filename to the
Expand Down
Loading