Unexpected PHP Files in WordPress Uploads: Inspect Before Deleting
An unfamiliar PHP file in uploads may be persistence, but its name alone is not proof. Preserve evidence, check context, and block execution using server rules.
You find something.php in wp-content/uploads, even though the directory is expected to contain images and documents. The file could be a web shell or loader that preserves an attacker’s access. But deleting every PHP file in uploads without checking can also remove a legitimate component from a plugin or custom application, break the website, and destroy evidence that would explain how the file was created.
The quick answer: treat an unfamiliar PHP file as a risk indicator, not a complete diagnosis. Preserve its path, metadata, hash, and related logs; do not open the file’s URL in a browser; ask the host to restrict execution or access if there is an immediate risk; then compare it with a backup and trace accounts, upload endpoints, plugins, themes, and jobs that can write there. Once you know its source, quarantine or remove it through a reversible plan, close the weakness, and test the server configuration that is actually in use.
Why files in uploads need careful investigation
wp-content/uploads commonly stores media that is publicly reachable. If PHP-FPM or Apache executes scripts in that directory, data that should be an image or document can become server-side code. An attacker may plant a file through a compromised account, a weak upload plugin, FTP credentials, or another vulnerability. However, a plugin or custom application may create subdirectories and helper files; location and filename are clues, not proof.
OWASP recommends allowing only necessary file types, validating content instead of trusting a browser-supplied Content-Type, using application-controlled filenames, limiting file sizes, and storing uploads where they cannot execute (File Upload Cheat Sheet). WordPress also recommends least-privilege permissions and warns against making directories world-writable with 777 (file permissions). Both guides support layered checks—not a one-line extension rule.
Stop exposure without destroying evidence
If the file has a public URL and there is evidence it can execute, contact the hosting provider promptly. Ask them to restrict the path or disable script handling there according to the stack in use. Do not type the URL into a browser, use curl that follows redirects, or ask a visitor to “check if it opens”. The provider can inspect access through logs or a controlled test on an isolated copy.
Before making changes, record:
- Its full path relative to the site root, filename, extension and letter case, size, owner/group, permissions, inode if available, and filesystem-reported creation/modification times.
- A SHA-256 hash and an evidence copy stored outside the web root with restricted access. Keep a separate working copy that can be scanned in an isolated environment.
- Related URL, response status,
Content-Type, request ID, time, IP information redacted according to policy, and relevant access/error log excerpts. - The last backup known to be clean, the plugin or theme that manages uploads, accounts that can write to the directory, and previous changes.
Do not alter timestamps or execute the file. If its contents need analysis, have an analyst do so in an isolated environment. A file may contain passwords, customer data, or dangerous code; do not paste raw contents into a public forum.
Inventory files without executing them
On a filesystem copy or a host-approved read-only shell session, list common script types and variations:
find wp-content/uploads -type f \( -iname '*.php' -o -iname '*.phtml' -o -iname '*.phar' -o -iname '*.php5' -o -iname '*.php7' \) -print
Run it from the correct installation root; multisite, custom upload paths, symlinks, and hosting mounts can change the scope. The command prints filenames only; it does not establish that every result is malicious. Also note hidden files, double-extension names such as image.php.jpg, newly created subdirectories, and configuration files such as .htaccess or .user.ini that affect how the server handles content.
Compare findings with a clean backup and trusted plugin/theme packages. Consider ownership, permissions, parent paths, modification times, database references, and upload logs. Timestamps can be forged; matching hashes with a vendor package or backup is stronger evidence, but legitimate files still need context. If a custom application intentionally uses PHP in uploads, document who owns it and why it belongs there.
Trace how the file was created
- Audit upload paths. Review frontend forms, REST/AJAX endpoints, media plugins, import plugins, integrations, and FTP/SFTP access. Uploads should require appropriate capabilities, restrict allowed types, use safe names, and validate content on the server.
- Correlate times with logs. Look for POST requests to upload endpoints, administrator/hosting logins, requests to the new file, plugin changes, backup restores, and scheduled tasks in the same period. Do not rely on file timestamps alone.
- Check persistence outside the directory. Look for code that references the path, unfamiliar accounts, changed plugins/themes, cron, database records,
.htaccess,.user.ini, PHP configuration, automated backups, and CDN rules. One file may only be a loader; another component may recreate it. - Use the right integrity check.
wp core verify-checksumscompares WordPress core, not uploads.wp plugin verify-checksums --allcan help with supported WordPress.org plugins; premium/custom components and application data need another trusted comparison source. See the WP-CLI security-check guide. - Establish visitor impact. Ask the host whether PHP handling is enabled on that path and whether public requests received output other than media. Do not prove it by executing a dangerous file in production.
Distinguish a file’s presence, reachability, and execution
Finding a filename in a directory does not prove the server executed it. Conversely, a 403 or 404 from one browser does not prove the file cannot be reached through another origin, path variation, CDN, or handler rule. Do not ask visitors to test its URL. Ask the provider to confirm server handler mapping and compare origin logs with edge logs; if necessary, run a provider-approved canary test on staging—not by creating a web shell.
On a read-only evidence copy, metadata commands such as stat and sha256sum can tie a finding to a hash and timestamp without executing file contents:
stat --printf='%n %s %U:%G %a %y' -- 'wp-content/uploads/path/suspect.php'
sha256sum -- 'wp-content/uploads/path/suspect.php'
Store the output in a restricted ticket and note the operating system or utility version if relevant. If the host does not provide shell access, ask it to export metadata and logs through its official support channel. Do not send a copy to a public analysis service before confirming it contains no personal data, secrets, or contract-restricted material.
Remove or quarantine only after the source and evidence are clear
If the file is confirmed unauthorized, preserve evidence, block public access, and then quarantine it outside the web root with restricted permissions or remove it according to the host’s incident process. Do not leave a quarantine copy in a directory still served by the web server. After removal, recheck the folder structure, symlinks, extension variations, and the accounts/processes that can write there. If it returns, stop repeating the deletion and trace persistence as part of a broader WordPress hack response; involve the technical team as needed.
Do not restore the entire uploads directory from an old backup without comparing newly uploaded business media and checking whether the backup was already compromised. List assets that must be retained, hash important files, and perform a selective restore on a test copy. Review database references before restoring legitimate files. Before choosing a restore point, test the backup and confirm it predates the compromise; see our guide to website backups you can actually restore.
Prevent PHP execution from uploads
Ask the server administrator to apply controls that match the stack—Apache, Nginx, LiteSpeed, containers, or managed hosting. One .htaccess snippet does not work everywhere: Nginx does not read .htaccess, and plugins or multisite may use different rules. Confirm that a control is placed in the right configuration, will not be overwritten by WordPress-managed rules, and will not break legitimate upload/media features.
Test on staging with a harmless static file and an approved account: media should remain available as required, while an unauthorized script must not execute. Use a test file that contains no executable code; never upload a web shell as a test. Check that origin and CDN enforce the same policy and that caches do not serve an old copy. The host may also move uploaded data outside executable paths, restrict write permissions, allow only business-required file types, and alert on newly created files.
Common mistakes that make the incident worse
- Deleting a file before recording its hash and logs, losing evidence of the entry point.
- Assuming every
.phpfile is malware, or that a name likeimage.php.jpgis automatically safe. - Visiting a URL to “confirm PHP works”, which may execute harmful code or trigger a redirect.
- Copying Apache rules to Nginx, adding a deny rule without a backup, or editing production configuration without host approval.
- Scanning only uploads while missing a database record, plugin, administrator, server rule, or cron job that recreates the file.
- Setting every permission to
777or making the upload folder writable by every process. - Installing a scanner and deleting every alert without distinguishing false positives, vendor files, and incident evidence.
Should every PHP file in uploads be deleted? Not automatically. Check its owner, purpose, plugin source, hash, and logs; a custom application may have a legitimate file. Remove it only after confirming that it is unnecessary or harmful and preserving evidence.
Is a .php.jpg filename safe? Not necessarily. A server, handler, plugin, or incorrect rule may interpret names and extensions differently. Do not test by executing the file; ask the host to inspect handler mapping and use a safe copy.
Does wp core verify-checksums scan uploads? No. It compares core files. Uploads, unsupported plugins, custom themes, server configuration, and the database require other checks.
When should I escalate? Immediately if the file is publicly accessible, login/payment is affected, it returns after removal, logs are unavailable, or you cannot restrict execution without risking an outage.
If an unfamiliar file appears in uploads after a suspected compromise, request a WordPress security audit to trace the entry point, file changes, and server controls without deleting evidence at random.
Does any of this look like your website?
Tell us on WhatsApp. We look first, explain what needs fixing, then you decide. For a fuller picture, request the free audit.
Related articles.
How to Find a WordPress Backdoor When Malware Keeps Returning
Malware that returns after cleanup usually means persistence or the entry point remains. Trace the path with evidence instead of deleting files at random.
Hacked Content vs Security Issues vs Google Manual Actions
Google warnings are not interchangeable. Learn what Security Issues, Manual Actions, and Safe Browsing signals mean before choosing a recovery path.
How to Safely Trace a Referrer-Based WordPress Redirect
Does a WordPress URL redirect only from Google or a particular link? Reproduce it safely, record the first response, and trace the rule without exposing visitors.