Make WordPress Core

Opened 3 days ago

Last modified 3 days ago

#66409 new defect (bug)

REST API: Raw-body media upload leaves its temporary file behind when the multisite size check fails

Reported by: ExtremsX Owned by:
Priority: normal Milestone: Awaiting Review
Component: REST API Version: 4.9.8
Severity: normal Keywords: has-patch has-unit-tests
Cc: Focuses:

Description

WP_REST_Attachments_Controller::upload_from_data() writes the request body to a file created by wp_tempnam() and then calls check_upload_size(). When that check fails (file too big, not enough upload space, or the user is over quota), the method returns the error without deleting the file:

$size_check = self::check_upload_size( $file_data );
if ( is_wp_error( $size_check ) ) {
        return $size_check;
}

So on a multisite network with upload limits enabled, every rejected raw-body upload (POST /wp/v2/media with the file as the request body) leaves a copy of its body in the temporary directory. A user who can upload files can repeat this to fill the disk, which is what the quota is meant to prevent.

The same method already deletes the file when wp_handle_sideload() fails, and the URL sideload path deletes it after the same check_upload_size() failure.

Steps to reproduce

  1. Multisite, Network Settings: enable the upload space check and set "Max upload file size" to 1 KB (fileupload_maxk = 1, upload_space_check_disabled = 0).
  2. As a user who can upload files, send a raw-body upload larger than 1 KB:
    curl -u user:application-password -X POST https://example.com/wp-json/wp/v2/media \
      -H 'Content-Type: image/png' \
      -H 'Content-Disposition: attachment; filename="test.png"' \
      --data-binary @five-kb-file.png
    
  3. The response is 400 rest_upload_file_too_big, and get_temp_dir() now holds test-XXXXXX.tmp with the full body. Each request adds one more.

Reproduced on 7.1.4 (multisite, subdirectory, PHP 8.2); trunk has the same code.

Patch

Delete the temporary file before returning the size error, plus a test that the file is gone after a rejected upload. PR to follow.

Found with code-intruder, my own static analysis tool, which flags temporary files left behind on an exit path. AI tools (Claude Code) helped confirm and reproduce it; I reviewed and tested the change myself.

Change History (3)

This ticket was mentioned in ​PR #15134 on ​WordPress/wordpress-develop by ​@ExtremsX.


3 days ago
#1

WP_REST_Attachments_Controller::upload_from_data() writes the request body to a
file from wp_tempnam() before calling check_upload_size(). When that check
fails on multisite (file too big, not enough upload space, or user over quota),
the method returned the error without deleting the file, so every rejected
raw-body upload left a copy of its body in the temporary directory.

This deletes the file before returning, as the method already does when
wp_handle_sideload() fails and as the URL sideload path does after the same
check.

Adds test_create_item_with_data_exceeds_multisite_max_filesize_removes_temp_file()
(ms-required), which checks that no canola-*.tmp is left after a rejected upload.

Tested on a local 7.1.4 multisite (subdirectory): with fileupload_maxk = 1, a
5 KB raw-body upload returned 400 rest_upload_file_too_big and left one
.tmp (5120 bytes) in the temp dir; with the change, the same request leaves none.

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code, with code-intruder (my own static analysis tool)
Model(s): Claude Opus, Claude Haiku
Used for: code-intruder flagged the temporary file left on the error return and
an AI review triaged the finding. AI helped reproduce it on a local multisite
install (REST request, checking the temp dir before/after) and write the test.
I reviewed and tested the change myself.

​@umesh.nevase commented on ​PR #15134:


3 days ago
#3

Test Report

This report validates that the indicated patch addresses the issue.

Patch tested: ​https://github.com/WordPress/wordpress-develop/pull/15134

Environment

  • WordPress: PHPUnit: 7.2-beta3-64914-src (trunk d49cecb0f0 + PR commit 74f05b7160). Manual: 7.2-beta2-20261028.171134 build (multisite, subdirectory install), with the PR's controller file copied in.
  • PHP: 8.4.27
  • Server: nginx (Laravel Valet)
  • Database: MariaDB 11.7.2
  • Browser: N/A (REST API requests via curl)
  • OS: macOS (Darwin 25.6.0)
  • Theme: N/A
  • Plugins: None activated

Steps taken

1. PHPUnit

  1. Checked out the PR branch on top of trunk.
  2. Ran: WP_MULTISITE=1 phpunit -c tests/phpunit/multisite.xml --filter test_create_item_with_data_exceeds_multisite_max_filesize
  3. Reverted only the source change in class-wp-rest-attachments-controller.php (keeping the new test) and re-ran, to confirm the test fails without the fix.
  4. Restored the patch, re-ran the test, then ran the REST attachments controller tests.

2. Manual test (multisite)

  1. In Network Settings, set Max upload file size to 1 KB and enabled the upload-space check.
  2. Logged in as an administrator and sent POST /wp-json/wp/v2/media with the raw body of a ~198 KB JPEG (Content-Type: image/jpeg, Content-Disposition: attachment; filename=canola.jpg).
  3. Checked the PHP temp directory for leftover canola-*.tmp files.
  4. Repeated the request with the PR's class-wp-rest-attachments-controller.php in place.

Expected Results

When a raw-body REST upload is rejected by the multisite size check, the temporary file is removed.

Actual Results

  1. PHPUnit, without the patch: test_create_item_with_data_exceeds_multisite_max_filesize_removes_temp_file fails, because a leftover canola-*.tmp file remains after the rest_upload_file_too_big error.
  2. PHPUnit, with the patch: the test passes (2 tests, 9 assertions).
  3. PHPUnit, with the patch: the REST attachments controller tests pass (233 tests, 1268 assertions, 2 skipped), so no regressions were found.
  4. Manual, without the patch: the API returns rest_upload_file_too_big (400), but one new canola-*.tmp file is left in the temp dir.
  5. Manual, with the patch: the same 400 response is returned and no temp file is left behind.

Additional Notes

  • The code path only runs on multisite, because check_upload_size() returns early on single-site installs.
  • Not tested manually: the rest_upload_limited_space and rest_upload_user_quota_exceeded branches. They use the same early return, so the same change covers them.
Note: See TracTickets for help on using tickets.