2026年11月12日

2026年11月12日

Block PHP Execution in the WordPress Uploads Directory

This article is a localized edition of the Japanese original. Read the Japanese original

Introduction

wp-content/uploads/ is the directory where WordPress stores images and other media. If PHP files can be placed in this directory and executed, an attacker can upload a web shell (backdoor) and run arbitrary code.

Symptoms and Causes

  • A malware scan found PHP files inside wp-content/uploads/
  • A security plugin warned that PHP can be executed in the uploads directory
  • A suspicious file was found at a path such as wp-content/uploads/2024/01/shell.php
  • A web shell was planted by exploiting a vulnerability in a file upload feature

How to Fix It

Step 1: Find existing suspicious PHP files

# Search for every PHP file in the uploads directory
find /var/www/html/wp-content/uploads -name "*.php" -type f
find /var/www/html/wp-content/uploads -name "*.phtml" -type f
find /var/www/html/wp-content/uploads -name "*.php5" -type f

# Check PHP files created or modified recently (within 7 days)
find /var/www/html/wp-content/uploads -name "*.php" -mtime -7

# Inspect file contents (patterns typical of malware)
grep -rl "eval(base64_decode" /var/www/html/wp-content/uploads/
grep -rl "system\|exec\|passthru\|shell_exec" /var/www/html/wp-content/uploads/

# Delete suspicious PHP files
find /var/www/html/wp-content/uploads -name "*.php" -delete
find /var/www/html/wp-content/uploads -name "*.phtml" -delete

Step 2: Block PHP execution on Apache (.htaccess)

# Create wp-content/uploads/.htaccess
cat > /var/www/html/wp-content/uploads/.htaccess << 'EOF'
# Completely block execution of PHP files
<FilesMatch "\.(php|phtml|php3|php4|php5|php7|phar|shtml)$">
    Order Deny,Allow
    Deny from all
</FilesMatch>

# Turn off the PHP engine itself (when using mod_php)
<IfModule mod_php.c>
    php_flag engine off
</IfModule>

# Also block CGI execution
Options -ExecCGI
RemoveHandler .php .phtml .php5 .phar

# Also disable directory listing
Options -Indexes
EOF

chmod 644 /var/www/html/wp-content/uploads/.htaccess
# Check after configuring
# Create a test PHP file and try to access it
echo "<?php echo 'test'; ?>" > /var/www/html/wp-content/uploads/test_block.php
curl -o /dev/null -s -w "%{http_code}" https://example.com/wp-content/uploads/test_block.php
# A 403 means it worked
rm /var/www/html/wp-content/uploads/test_block.php

Step 3: Block PHP execution on Nginx

# /etc/nginx/sites-available/example.com
server {
    # ... existing settings ...

    # Block PHP execution in the uploads directory
    location ~* /wp-content/uploads/.*\.(php|phtml|php5|phar)$ {
        deny all;
        access_log off;
        log_not_found off;
    }

    # Place this before the PHP-FPM location block
    location ~ \.php$ {
        # Normal PHP handling
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}
# Test the Nginx configuration and apply it
sudo nginx -t && sudo systemctl reload nginx

# Check
curl -o /dev/null -s -w "%{http_code}" https://example.com/wp-content/uploads/shell.php
# A 403 means it worked

Step 4: Block PHP uploads in WordPress too

// functions.php: block uploading PHP files in the first place

// (1) Add PHP extensions to the blocked upload list
add_filter('upload_mimes', function(array $mimes): array {
    // Remove PHP-related MIME types (just in case)
    unset($mimes['php'], $mimes['phtml'], $mimes['phar']);
    return $mimes;
});

// (2) Detect and reject PHP in uploaded files
add_filter('wp_handle_upload_prefilter', function(array $file): array {
    $dangerous_extensions = ['php', 'phtml', 'php3', 'php4', 'php5', 'phar', 'shtml'];
    $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));

    if (in_array($ext, $dangerous_extensions, true)) {
        $file['error'] = 'Uploading PHP files is not allowed.';
    }

    // Check the MIME type as well
    if (str_contains($file['type'], 'php')) {
        $file['error'] = 'Uploading PHP files is not allowed.';
    }

    return $file;
});

Step 5: Set up regular monitoring

# Script that regularly checks for PHP files in uploads
cat > /usr/local/bin/check-uploads-php.sh << 'EOF'
#!/bin/bash
UPLOADS_DIR="/var/www/html/wp-content/uploads"
ADMIN_EMAIL="admin@example.com"

PHP_FILES=$(find "$UPLOADS_DIR" -name "*.php" -o -name "*.phtml" -o -name "*.phar" 2>/dev/null)

if [ -n "$PHP_FILES" ]; then
    echo "Warning: PHP files were detected in the uploads directory:
$PHP_FILES" | mail -s "[URGENT] A web shell may have been planted on WordPress" "$ADMIN_EMAIL"

    # Automatic deletion (use with care)
    # find "$UPLOADS_DIR" -name "*.php" -delete
fi
EOF
chmod +x /usr/local/bin/check-uploads-php.sh

# Check every hour
echo "0 * * * * root /usr/local/bin/check-uploads-php.sh" > /etc/cron.d/wp-uploads-php-check

Caveats

  • php_flag engine off in .htaccess only works when you use mod_php. If you use php-fpm (FastCGI), handle it with the <FilesMatch> directive or an Nginx location block
  • On Nginx, if you do not place the uploads block rule before location ~ \.php$, the latter may take precedence and the PHP file can still run. Always put the block rule first
  • Some plugins (for example, Slider Revolution) may place .php files inside uploads. Check what a file is for before deleting it, and remove plugins you no longer need before taking action

Summary

To block PHP execution in the uploads directory: (1) find and delete existing suspicious files with find wp-content/uploads -name ".php"; (2) on Apache, add <FilesMatch "\.(php|phar)$"> Deny from all to uploads/.htaccess; (3) on Nginx, set location ~ /wp-content/uploads/.*\.php$ to deny all; (4) reject PHP uploads themselves with the wp_handle_upload_prefilter hook; (5) monitor uploads for PHP files with an hourly cron job.

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲