WordPress: a delete-a-file bug escalated to RCE

The Sonar writeup of WordPress's file-delete-to-RCE is one of the cleanest demonstrations of why "low impact" file-deletion bugs deserve full attention. The original disclosure showed how an authenticated user, with no special privileges beyond uploading media, could escalate to arbitrary code execution through a sequence of mundane-looking primitives.

The chain, simplified

The bug gave an attacker the ability to delete files inside the WordPress installation. That, on paper, is a defacement / availability issue. In practice, here's how it became RCE:

Delete .htaccess. WordPress installs ship an .htaccess in wp-content/uploads/ that prevents Apache from executing PHP files in that directory. Delete that file and the directory is back to default — anything inside it is now executable.

Upload a crafted PHP file via the media uploader. Image uploads run through a sanitisation pipeline, but you can stash PHP into the EXIF or comment metadata of an image and let the file land on disk with a .php extension if you have control over the filename — which the bug allowed.

Visit the uploaded file's URL. PHP runs.

No part of that chain is novel. Each primitive is "small" on its own. The composition is the whole game.

Why this pattern keeps coming back

Sanitisation pipelines focus on content, not on what the file is allowed to do. A pipeline that strips active content from an image is doing useful work — but it doesn't decide whether the destination directory should execute scripts.

The defense-in-depth on uploads is fragile. A single .htaccess, a single php_admin_flag engine off in the vhost, a single nginx location block — knock any one of them out and execution comes back.

"Authenticated low-privilege" is a wide door. Sites with open registration, comment-author roles, or WooCommerce customer accounts give that role away to anyone.

How to harden against it

Block PHP execution in uploads at the server level, not just at the application level. Apache <FilesMatch> directive in the vhost, nginx location ~ \.php$ { deny all; } inside /wp-content/uploads/. Don't rely on .htaccess alone — if an attacker can delete it, your only defense is gone.

Read-only uploads where possible. For many marketing sites, nothing legitimate needs to land in uploads after launch. Mount the directory read-only and use a separate write path for the admin.

Cap mime types at the web server level. Even if WordPress thinks the file is an image, the server can decide to refuse to serve anything that isn't.

Watch for .htaccess modifications. A monitoring rule that fires whenever an .htaccess inside the WordPress tree changes will catch this and a dozen other variants.

If you'd like the upload-hardening review done as a standalone, that's part of my hardening service. If you suspect a site already went through this kind of chain, open an engagement and I'll diff the file tree.

Source: Sonar — WordPress File Delete to Code Execution.