HexBrain

WordPress · Report #3923967

WordPress 7.1 Media Processing Race Enables Persistent PHP File Write and Author-to-RCE

Validstatus
highseverity
37upvotes
21 Oct 2026disclosed

How it was found

Hi Wordpress security team,

We are submitting this report in good faith and in the best interest of preventing this issue from shipping in the upcoming WordPress 7.1 release.

The vulnerability is not present on the current WordPress 7.0 branch (7.0.3), , while we have confirmed it in the WordPress 7.1 development line from Beta 2 through the current development source build we tested (7.1-RC2-63095-src).

WordPress 7.1 is currently scheduled for release on August 19, 2026. We are therefore reporting this before the final release so the issue can be reviewed before it reaches stable users.

If issues affecting pre-release/development versions are considered outside the scope of the HackerOne program, we respectfully ask that you allow us to withdraw/close the report from our side rather than resolving it as N/A.

Pre-condition

For the demonstrated direct-HTTP execution path, WordPress's selected temporary directory must be located under a PHP-served path.

If /tmp is not writable by the user running PHP, WordPress can fall back to another writable temporary directory. In our Docker reproduction, making /tmp non-writable for the PHP user causes WordPress to fall back to the wp-content directory.

We simulate this condition in Docker by making /tmp writable only by root.

One deployment scenario where this condition can occur is a containerized environment using readOnlyRootFilesystem, depending on how /tmp is mounted and configured. If /tmp is not provided as a writable tmpfs/emptyDir for the web/PHP user, WordPress may select another writable fallback directory.

Vulnerability

Under the validated preconditions, an authenticated user with the default Author role can exploit a race condition to achieve persistent arbitrary PHP execution.

By combining the media finalization, URL-media sideload, and image post-processing flows, the attacker can cause WordPress/Imagick to generate persistent image files with a .php extension while the image XMP metadata contains valid PHP code.

The race condition leading to RCE involves three REST actions in addition to the initial image upload.

Step 1

First, an image is uploaded using:

POST /wp-json/wp/v2/media?generate_sub_sizes=false

Step 2

Call:

POST /wp-json/wp/v2/media/{id}/finalize

This poisons the seed attachment metadata so original_image points to the attacker-chosen temporary .php path.

Step 3

Call:

POST /wp-json/wp/v2/media

with the url parameter.

This causes WordPress to fetch the attacker-controlled JPEG. A remote Content-Disposition header causes the predictable .php basename to temporarily exist in WordPress's selected temporary directory.

Race the above request with:

POST /wp-json/wp/v2/media/{id}/post-process?action=create-image-subsizes

The /post-process request races image processing against the temporary .php JPEG. When the race succeeds, Imagick opens the file as an image and WordPress generates persistent .php image derivatives.

Vulnerable Version

Affected: WordPress 7.1-RC2-63095-src — confirmed

Also reproduced in the WordPress 7.1 development line beginning with Beta 2.

Not affected: current WordPress 7.0 branch (7.0.3-src) in our testing.

Root cause of vulnerability :

  1. /wp-json/wp/v2/media - the URL-media flow downloads an attacker-controlled remote image through download_url(). download_url() honors the remote Content-Disposition: attachment; filename=malicious.php value and renames the local temporary file to that attacker-influenced basename, allowing a valid image to briefly exist with an executable extension such as .php.

    URL-media calls download_url() — lines 625–638

  2. /wp-json/wp/v2/media/{seed_id}/post-process?action=create-image-subsizes races against the temporary file and invokes wp_update_image_subsizes() - which obtains the original-image path from attachment metadata and passes it into the image editor.

    Original path consumed for sub-size generation If the extension does not identify a known MIME type, wp_get_image_editor() deliberately allows the selected image editor to determine the format from the file contents instead of rejecting it based on extension. The derivative filename generator reuses the source extension when no replacement extension is supplied, allowing .php to propagate into generated sub-size filenames.

    Derivative extension reuse

  3. /wp-json/wp/v2/media/{id}/finalize allows attacker-controlled filename/path metadata to be persisted and later treated as a trusted local image path. The route accepts file and original_image as unrestricted strings, and finalize_item() directly assigns the supplied original_image into attachment metadata without sufficient validation.

    Finalize route schema

    Direct original_image metadata assignment


Steps to Reproduce

  1. Clone the WordPress development repository and check out the exact tested source revision:

    git clone https://github.com/WordPress/wordpress-develop.git
    cd wordpress-develop
    git checkout 3ca084eb3399222a08cc0573278eed6be0f80714
    

    Verify the version in:

    wordpress-develop/src/wp-includes/version.php
    

    Expected version:

    7.1-RC2-63095-src
    

    WP version bump commit:

    https://github.com/WordPress/wordpress-develop/commit/3ca084eb3399222a08cc0573278eed6be0f80714

  2. Install dependencies:

    npm install
    
  3. Build WordPress:

    npm run build:dev
    
  4. Start the Docker environment:

    npm run env:start
    
  5. Install WordPress:

    npm run env:install
    
  6. Create an Author user:

    npm run env:cli -- user create attacker [email protected] \
      --role=author \
      --user_pass='Password123!'
    
  7. Verify the user was created:

    npm run env:cli -- user list
    
  8. Make /tmp non-writable for non-root users:

    docker exec -u root wordpress-develop-php-1 chmod 755 /tmp
    
  9. Verify the /tmp permissions:

    docker exec -u root wordpress-develop-php-1 ls -ld /tmp
    

    Expected permissions should resemble:

    drwxr-xr-x
    
  10. From the directory containing the reproducer, run:

    python3 race_poc.py
    

    The PoC uses the crafted image payload hosted at:

    http://140.245.237.219/payload.jpg
    

    The file is approximately 50 MB and contains PHP code inside its image metadata that executes:

    phpinfo();
    

    as proof of PHP code execution.

  11. To generate the crafted image payload locally instead, use the attached generator:

    python3 gen_payload.py
    

    Ensure the generated file is served with the following HTTP response headers:

    Content-Type: image/jpeg
    Content-Disposition: attachment; filename="payload.php"
    

Impact

Under the validated preconditions, an attacker with a default Author account can achieve persistent remote code execution on the WordPress server.

Successful exploitation creates PHP-executable image derivatives that:

  • Persist after the temporary source file is deleted.
  • Can be invoked over HTTP without authentication.
  • Execute with the privileges of the PHP/web-server user.
  • May expose database credentials, authentication salts, private files, and other application secrets.
  • Can enable database manipulation, administrator account creation, content compromise, malware persistence, and lateral movement.

The demonstrated RCE requires Imagick, a PHP-served writable temporary directory, PHP execution in the resulting location, and successful exploitation of the race window.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original