The wp-content/uploads directory is meant for media only — images, video, PDFs, audio. WordPress core never writes a PHP file there, and no legitimate plugin should either. When the audit flags a PHP file inside this directory, the most likely explanation is that an attacker successfully uploaded a webshell — a small script that grants remote control over the server.
Why this matters
A webshell in uploads is not a theoretical risk. Once the file lands, the attacker can execute arbitrary shell commands, read wp-config.php to extract the database password, create a hidden administrator account, inject spam links into existing posts, or enroll the site in a botnet that sends phishing emails. We have seen worse: skimmers planted on WooCommerce checkout pages that exfiltrate credit card numbers, full database dumps stored in a publicly browsable folder, and pivoting attacks where the compromised site is used as a staging point to attack other sites on the same shared host. Even after you delete the first webshell, the attacker has almost certainly placed a second or third backdoor elsewhere — partial cleanup is not a fix.
How to detect
Typical telltale signs: random 5–10 character file names (a8f2k.php), names that mimic core files (wp-cache.php, wp-tools.php, class-wp.php), files dropped inside uploads/YYYY/MM/ alongside images, or unusual extensions such as .phtml, .phar, and .php7. Inside the file you will usually see a combination of eval, base64_decode, gzinflate, and str_rot13 — functions used to obfuscate the real payload. A classic one-liner is eval(base64_decode($_POST["cmd"])) — a single line that gives the attacker full command execution.
How to fix
Do not start by deleting. The correct sequence is: full backup of files and database (the malicious code is potential forensic evidence), download every suspicious file to an isolated text editor for review, run a full scan with Wordfence or MalCare to find additional backdoors, and only then delete. After cleanup, rotate every credential — database, admin users, FTP, hosting panel — and regenerate the salts in wp-config.php. Update core, themes, and all plugins to their latest versions before bringing the site back to normal.
To prevent recurrence, block PHP execution inside the entire uploads tree at the web server layer:
<FilesMatch "\.(php|phtml|phar|php7|php8)$">
Require all denied
</FilesMatch>
location ~* /wp-content/uploads/.*\.(php|phtml|phar)$ {
deny all;
}
Common mistakes
Mistake one: deleting only the file the audit reported and assuming the job is done. An experienced attacker plants three to five backdoors in different locations — inside the active theme, inside functions.php, sometimes even serialized into the wp_options table as a fake cron callback. Mistake two: trusting filenames. wp-cron-fix.php sounds harmless but it is not a core file. Every PHP file under uploads is suspicious until proven otherwise. Mistake three: skipping the password rotation because "only one file was removed" — before deleting, the attacker almost certainly read wp-config.php, so the existing database credentials must be considered leaked.
Verifying the fix
Run find wp-content/uploads -name "*.php" -type f on the server. The only matches should be empty index.php files containing nothing but the "Silence is golden" comment. Test with curl by uploading a temporary test.php: curl -I https://example.com/wp-content/uploads/test.php should return 403, never 200. Finish with a full Wordfence or MalCare scan and confirm the "Critical issues" counter is zero.