Webshells in the Uploads Folder: How to Block PHP Execution

Why webshells end up in wp-content/uploads, how to find them and how to block PHP execution on Apache, LiteSpeed and NGINX, and what the block does not cover.

An uploads folder with images and a red PHP file, blocked by a large prohibition sign

The wp-content/uploads folder is the one place in a WordPress installation where the site needs to be able to write freely: images, PDFs and plugin-generated files all end up there. That is exactly why it is also the favorite spot for webshells, small PHP files that, when requested from a browser, let an attacker run commands, upload more files and browse the server.

Why uploads are the target

A file upload vulnerability in a plugin, a misconfigured contact form or a compromised author account is enough to write a file into uploads. If the server also executes PHP files in that folder, the uploaded file immediately becomes an entry point. Webshells often have names that blend in, such as cache.php, thumb.php or a random name inside a year/month folder, and sometimes a forged modification date to make them look old.

The uploads folder of a normal WordPress site should not contain any PHP files. Exceptions are rare, involve only a few plugins, and should be checked one by one.

Finding executable files

The search has to cover every extension the server might interpret as PHP, not just .php:

find wp-content/uploads -type f \( -iname '*.php*' -o -iname '*.phtml' \
  -o -iname '*.phar' -o -iname '.htaccess' -o -iname '.user.ini' \)

The list should also include .htaccess and .user.ini files: an attacker creates them to re-enable PHP execution or to load a file on every request through auto_prepend_file. And a file with an image extension is not necessarily harmless: it may contain PHP code that another file includes.

Blocking PHP execution

With Apache, and with LiteSpeed, which reads its .htaccess files, all you need is an .htaccess file inside the uploads folder:

# wp-content/uploads/.htaccess (Apache 2.4 and LiteSpeed)
<FilesMatch "\.(?i:php[0-9]?|phtml|phar)$">
    Require all denied
</FilesMatch>

With NGINX the rule goes in the virtual host configuration, and it must appear before the generic block that passes PHP files to PHP-FPM, because NGINX uses the first regular expression that matches:

location ~* ^/wp-content/uploads/.*\.(php[0-9]?|phtml|phar)$ {
    deny all;
}

After the change, it is worth actually testing the result: create a test PHP file in uploads, request it from the browser and check that the response is a 403 error and not the execution of the code. Then delete the test file.

What the block does not do

Blocking direct execution is an important measure, but it has precise limits. It does not delete webshells that are already there, which still have to be found and removed. It does not stop a PHP file in another folder from including a file hidden in uploads, because the inclusion happens inside PHP and never goes through the web server. And if the rule lives in an .htaccess that the site user can modify, an attacker who already has write access can remove it: for this reason, where possible, it is better to put it in the server configuration.

The block should therefore be seen as one more layer, alongside plugin updates, correct permissions and file monitoring. If you find a suspicious file and do not know what it does, do not open it in the browser: write to us and we will analyze it without running it.

THE NEXT STEP

Is your site
infected?

Every hour counts: the malware keeps hitting visitors and customers, and Google, your host and the blacklists won’t wait. Choose a plan, pay online and share your access: the cleanup starts right away, at a fixed price and with guaranteed times.

Request a cleanup

Takes you to the plans: cleanup within 6, 12 or 24 hours of receiving access.

MANAGED SERVER S.R.L.info@malwarecleanup.com +39 02 5656 9681

Monday – Friday · 9:30am – 7:30pm Italian time (CET)

Prefer an online form?
Contact us at managedserver.it ↗