Skip to content

REST API: Bound the size of media sideloaded from a URL - #12825

Closed
adamsilverstein wants to merge 4 commits into
WordPress:trunkfrom
adamsilverstein:fix/65517-sideload-size-limit
Closed

REST API: Bound the size of media sideloaded from a URL#12825
adamsilverstein wants to merge 4 commits into
WordPress:trunkfrom
adamsilverstein:fix/65517-sideload-size-limit

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Aug 4, 2026

Copy link
Copy Markdown
Member

Follow up to [62841], addressing the two observations @courane01 made while testing 7.1-beta4 in https://core.trac.wordpress.org/ticket/65517#comment:17.

check_upload_size() returns early when ! is_multisite(), so on single site - the common case - the URL sideload path has no size ceiling at all. upload_max_filesize and post_max_size bound a request body, not a fetch the server makes itself, so nothing else was stopping a url parameter from pulling in a file of any size. This applies wp_max_upload_size() on that path, so a URL can't bring in a file larger than the same site would accept as a direct upload.

The second half is that the check ran after download_url() had already streamed the whole file to disk. The limit is now also passed to the request as limit_response_size, which stops the transfer once it's passed, so an oversized remote file never lands on disk in full. One byte over the ceiling is enough to fail the size check, so the file is still rejected. Compression is already disabled for streamed requests, so this doesn't change how the response is decoded.

The multisite checks are untouched and still run first, so rest_upload_file_too_big and rest_upload_limited_space are returned as before for the network file size limit and the site space quota. Sites that want a different ceiling can use the existing upload_size_limit filter.

Trac ticket: https://core.trac.wordpress.org/ticket/65517

How has this been tested

Automated:

npm run test:php -- --filter 'create_item_from_url' --group restapi
npm run test:php -- -c tests/phpunit/multisite.xml --filter 'create_item_from_url' --group restapi
  • Two new tests: one that a file over the limit is rejected on single site with rest_upload_file_too_big, and one that the download request is capped at wp_max_upload_size() + 1. Both fail on trunk without the change - I checked by stashing the source change and re-running.
  • Existing sideload tests: 15 pass on single site, 17 on multisite, including the two multisite tests from [62841].
  • Full WP_Test_REST_Attachments_Controller class: 190 tests, 2 skipped, 0 failures.
  • phpcs --standard=phpcs.xml.dist clean on both changed files.

Types of changes

  • Apply wp_max_upload_size() as a ceiling on the URL sideload path, so it applies on single site as well as multisite.
  • Pass the ceiling to the download as limit_response_size so the transfer is bounded rather than inspected after the fact.
  • Add tests for both.

Open questions

  • Is wp_max_upload_size() the right ceiling here? It's derived from the PHP directives that govern request bodies, which don't really apply to a server-side fetch. The argument for it is parity: sideloading shouldn't accept a file the same user couldn't upload directly, and it's the number the media library already shows people. If we'd rather hosts be able to allow larger server-side fetches specifically, a dedicated filter would do it, though I'd lean toward not adding new API this late in 7.1.
  • limit_response_size truncates rather than erroring, which is why the ceiling is set one byte high and the size check still does the rejecting. That works, but it does mean the transport's truncation behavior is load-bearing. Happy to drop it and keep only the post-download check if that feels too clever for a late-cycle fix.

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5
Used for: Drafting the implementation and tests from the review notes on the ticket. I reviewed and verified the behavior and test results myself.

Commit message

REST API: Bound the size of media sideloaded from a URL.

`WP_REST_Attachments_Controller::create_item_from_url()` only ran `check_upload_size()`, which returns early when `! is_multisite()`, so a single site had no ceiling at all on this path: `upload_max_filesize` and `post_max_size` bound a request body, not a fetch the server makes itself. Apply `wp_max_upload_size()` to the download, so a URL cannot bring in a file larger than the same site would accept as a direct upload, and pass that limit to the request as `limit_response_size` so an oversized file is not written to disk in full before being rejected. The multisite checks are unchanged and still run first, and no ceiling is applied when `wp_max_upload_size()` returns 0.

Follow-up to [62659], [62841].

Props andrewserong, courane01.
Fixes #65517.

The attachments controller's URL creation path, `create_item_from_url()`, only ran `check_upload_size()`, which returns early when `! is_multisite()`. A single site therefore had no ceiling at all on this path: `upload_max_filesize` and `post_max_size` bound a request body, not a fetch the server makes itself, so any URL could pull a file of any size into the media library.

Apply `wp_max_upload_size()` to the download, so a URL cannot bring in a file larger than the same site would accept as a direct upload. The limit is passed to the HTTP request as `limit_response_size`, which stops the transfer once it is passed, so an oversized remote file is no longer written to disk in full before being rejected. Sites that need a different ceiling can adjust it with the existing `upload_size_limit` filter.

The multisite checks are unchanged and still run first, so `rest_upload_file_too_big` and `rest_upload_limited_space` continue to be returned for the network file size limit and the site space quota. No limit is applied when `wp_max_upload_size()` cannot determine a size.

Follow-up to [62659], [62841].

See #65517.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props adamsilverstein, andrewserong.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@andrewserong

Copy link
Copy Markdown
Contributor

Thanks for putting this up, I'd started to look at it and this is pretty much exactly where I'd landed, too 👍

Is wp_max_upload_size() the right ceiling here?

In my view, yes. The behaviour for url in this endpoint is effectively getting the server to perform an upload on behalf of the user, so I think making it behave as much as possible (i.e. with the same restrictions) as a user generated upload is the right call for 7.1.

limit_response_size truncates rather than erroring, which is why the ceiling is set one byte high and the size check still does the rejecting.

This too seems good to me, as it prevents the temp file from growing beyond the threshold, and it's a fairly simple change code-wise.

@andrewserong andrewserong left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is testing great for me, thank you for hardening this! My vote would be to get this in for 7.1.

To test locally, on a single test site, I added a cap at 100kb via:

const S65517_MAX_SIZE = 102400; // 100 KB

if ( null !== S65517_MAX_SIZE ) {
	add_filter( 'upload_size_limit', static fn() => S65517_MAX_SIZE, 20 );
}

Files under that sideloaded fine, and files over that size returned a pleasing error in the editor:

Image

LGTM!

Comment thread src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php Outdated
return $args;
};

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.

@adamsilverstein

adamsilverstein commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

I updated the errant doc block, this is "good to go".

@andrewserong

andrewserong commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Looks like this has been committed as of 5f5d96b. Good to close this PR out now?

@adamsilverstein

Copy link
Copy Markdown
Member Author

Yep, closing - fixed in https://core.trac.wordpress.org/changeset/63015

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants