PHP Files in wp-content/uploads: How They Get There and How to Stop Them

By · Updated · 5 min read

WordPress’s wp-content/uploads directory holds media: images, PDFs, videos, the occasional zip file. It is not supposed to hold executable code, and no part of core ever writes PHP there. So when you find a .php file sitting among your 2019 photos, the question isn’t whether something is wrong — it’s how it got in and what else it did.

Why a PHP file in uploads is always worth investigating

The uploads directory is the one place on a WordPress site that is both writable by the web server and reachable directly from a browser. That combination is what attackers want: a file they can place, and a URL they can hit to run it. A web shell in wp-content/uploads/2019/07/ is functionally a control panel for your server, reachable by anyone who knows the filename, and it doesn’t need a WordPress login to work.

It’s also excellent camouflage. Uploads folders on established sites hold thousands of files across dozens of dated subdirectories, nobody browses them by hand, and backup tools often skip them for size reasons. A single extra file with an unremarkable name can sit there for months.

How they get there

Four routes account for nearly all of it. A vulnerable upload handler in a plugin or theme that accepts a file without properly checking its type — old contact form and gallery plugins are the classic source, and my Contact Form 7 guide walks through one version of this in detail. An existing backdoor elsewhere in the install writing new files wherever it can, in which case the uploads file is a symptom rather than the entry point. A compromised admin account with the ability to upload or edit files directly. Or a server-level compromise — another site on the same shared hosting account, or a weak FTP or SSH credential.

Watch for disguised extensions too. Files named image.php.jpg, photo.jpg.php, file.phtml, or shell.php5 are attempts to slip past a filter that only inspects part of the filename, and on some server configurations they execute anyway. A file whose name looks like a normal upload but whose contents are PHP deserves exactly the same treatment as an obviously named one.

Finding every one of them

With shell access, this is a one-liner: find wp-content/uploads -type f -name "*.php*" lists every PHP-ish file in the tree, including the double-extension variants. Add a date filter such as -newermt "2026-01-01" once you know roughly when the compromise happened. It’s worth also searching for PHP content in files with innocent extensions — an image that contains <?php is not an image.

Without shell access, your host’s file manager will do it, though slowly, and most security plugins will flag PHP in uploads during a scan. Whichever tool you use, check the modification timestamps: compromised files usually cluster tightly around one date and time, and that cluster is the most useful piece of evidence you’ll get — it dates the breach and tells you which backup is safe to trust.

Don’t stop at the first one. Attackers plant several, in different folders, with different names, precisely so that finding one doesn’t end the story.

Removing them safely

Copy the files somewhere outside the web root before deleting, if you can — they’re evidence, and a decoded payload often tells you what else to check. Then delete them rather than renaming them in place, since renaming to .php.bak or .txt still leaves a readable copy that some configurations will happily execute anyway.

Legitimate media files are never PHP, so there’s no risk of deleting a real attachment here. The only genuine caution is that a small number of plugins do place PHP inside their own subdirectory under uploads — cache and optimization plugins in particular. If a file lives in a clearly plugin-named folder, check it against a fresh copy of that plugin before you assume the worst.

Block PHP execution in uploads

Deleting the files fixes today. Blocking execution fixes the pattern. On Apache, a small .htaccess file inside wp-content/uploads that denies requests for .php, .phtml, and numbered variants means that even a successful future upload can’t be run through a browser. On Nginx, the equivalent is a location block in the server config, which needs your host’s help on managed plans.

Two caveats. First, if the attacker also controls a backdoor elsewhere, they can simply rewrite or delete your .htaccess file — so this hardens the site, it doesn’t clean it, and the order matters. Second, test afterwards: a handful of caching, form, and page-builder plugins genuinely execute PHP from their own uploads subdirectory, and a blanket rule can break them. Deny broadly, then allow the specific path if something stops working. My .htaccess guide covers how attackers manipulate the same file, and the hardening guide covers where this fits alongside the rest.

The file isn’t the wound

A shell in uploads means something had write access to your server, and that something is still there until you prove otherwise. Check for the usual companions — other backdoors, admin accounts you didn’t create, and scheduled tasks — then update or remove whatever plugin allowed the upload, and rotate the credentials that could have been used. If the file keeps reappearing after you delete it, something is actively rebuilding it, which is covered in my reinfection guide.

If you’d rather have someone go through the whole install once and properly — every file, the database, the accounts, and the hole they came in through — that’s what my malware removal service does.

Common questions

Is every PHP file in wp-content/uploads malicious?

Nearly always, yes — WordPress core never writes PHP there, and media files are never PHP. The one exception worth checking is a plugin that creates its own subdirectory under uploads, which some caching and optimization plugins do. If the file sits in a clearly plugin-named folder, compare it against a fresh copy of that plugin before deleting.

Can I just delete the file and move on?

Deleting is the right immediate move, but it’s not the end. Something had write access to place it, and that access usually still exists — an unpatched upload handler, another backdoor, or a compromised account. Attackers also plant several copies in different folders, so search the whole tree rather than stopping at the first hit.

Will blocking PHP execution in uploads break my site?

Usually not — media files don’t need to execute. A small number of caching, form, and page-builder plugins do run PHP from their own subdirectory under uploads, so add the rule, then test the site properly: front end, admin, forms, and checkout if you have one. If something breaks, allow that one specific path rather than removing the rule.

How did they upload a PHP file if my forms only accept images?

Type checking is easy to get wrong. Old handlers check the extension but not the content, or check only the last extension in a name like photo.jpg.php, or trust the browser-supplied MIME type, which an attacker simply sets to image/jpeg. On some configurations a genuine image with PHP appended to it executes too.

Why do the files keep coming back after I delete them?

Because something on the site is recreating them — most often a backdoor in a theme or plugin file, a malicious WP-Cron event, or an injected row in the database that runs on page load. Recurrence within minutes or hours is a reliable sign the persistence mechanism was never found, not that the deletion failed.