What to do after a malware scan finds matches
Updated October 2026
A scanner tells you which files look malicious. Getting a site back to clean, and keeping it that way, takes a few more steps in the right order. This checklist is written for WPSniffer reports but applies to any file-based scan result.
1. Confirm each match
Open the report with the columns that explain the hit. For each row, read matched_text and the author's description, and decide: malware, suspicious, or false positive.
$ wpsniffer malware-scan --match-all --output-path ~/incident.csv \
--output-columns filename,signature_id,signature_description,rule_score,rule_author,rule_reference,matched_text \
/var/www/html
- Malware: PHP in
uploads, code in a core file thatwp core verify-checksumsalso flags, anything withevalof a decoded string, a password gate, or a rule named for a webshell family. - Suspicious: obfuscated code in a plugin you did install. Compare it with a fresh copy of the plugin from wordpress.org.
- False positive: a broad rule on a known, verified library. Silence it with
-e RULE_NAMEfor future scans. See handling false positives.
If the site is actively being abused (spam pages, redirects, phishing), take it offline or put it behind maintenance mode now, before continuing, so the damage stops while you work.
2. Preserve evidence, then quarantine
Do not delete. Copy the files and logs first; you will need them to find the entry point, and your host or a forensics team may ask for them.
$ mkdir -p -m 700 ~/incident-$(date +%F)
$ tar czf ~/incident-$(date +%F)/evidence.tgz -T <(cut -d, -f1 ~/incident.csv | tail -n +2 | sort -u)
$ cp /var/log/nginx/access.log* /var/log/apache2/access.log* ~/incident-$(date +%F)/ 2>/dev/null
Then move the confirmed files out of the web root and strip their permissions, which disables them without destroying anything:
$ mkdir -p -m 700 ~/quarantine
$ mv /var/www/html/wp-content/uploads/2026/10/cache.php ~/quarantine/ && chmod 000 ~/quarantine/cache.php
3. Find the entry point
Cleaning without closing the door means doing this again next week. Use the change time of the earliest malicious file and the access logs to find how it got there:
$ stat -c '%z %n' ~/quarantine/* | sort | head -1
$ grep -h 'cache.php' ~/incident-*/access.log* | head
$ grep -h 'POST' ~/incident-*/access.log* | grep -v 'wp-cron\|admin-ajax' | awk '{print $7}' | sort | uniq -c | sort -rn | head
Common entry points, roughly in order of frequency: a vulnerable or abandoned plugin or theme, a reused or weak admin password, a compromised FTP or hosting account, another infected site on the same server with shared write access, and an outdated WordPress core. The first request to the dropped file, or a burst of POSTs to one plugin endpoint just before its change time, usually names the culprit.
List the installed versions and check them against the plugin directory or a vulnerability database:
$ wp plugin list --fields=name,version,update_version,status --path=/var/www/html
4. Restore clean code
Replace rather than repair. Edited files can hide a second payload you did not notice.
- Core:
wp core download --force --version=$(wp core version)re-downloads every core file. Thenwp core verify-checksumsshould be silent. - Plugins and themes: delete and reinstall each one from its official source, at a current version. Remove anything you no longer use, especially abandoned plugins. Custom themes come from your own repository or a backup taken before the earliest malicious change time.
- Uploads: remove all PHP (see the uploads guide) and any
.htaccessyou did not create. wp-config.php,.htaccess,mu-plugins: read them line by line; checksums do not cover them.- Database: check
siteurlandhomeinwp_options, look for<scriptin posts and options, and remove unknown entries inactive_plugins.
A full restore from a backup predating the infection is a valid shortcut if you have one and can accept the data loss. Confirm the backup is clean by scanning it before you restore it.
5. Rotate every secret
Assume the attacker read wp-config.php and the database. Change:
- All WordPress passwords, starting with administrators; force a reset for every user if you cannot tell who was affected.
- The database password, and the value in
wp-config.php. - The authentication salts in
wp-config.php, which logs every session out. - SSH, SFTP, FTP and hosting control panel passwords and keys.
- API keys stored in plugins: payment gateways, mail services, CDN tokens, social logins.
6. Remove persistence
Attackers leave ways back in that are not files. Check:
$ wp user list --role=administrator --fields=ID,user_login,user_email,user_registered --path=/var/www/html
$ wp cron event list --path=/var/www/html
$ ls -la /var/www/html/wp-content/mu-plugins/
$ crontab -l; ls -la /etc/cron.d /etc/cron.daily
$ find /var/www/html -name '*.php' -newer /var/www/html/wp-config.php -not -path '*/wp-content/cache/*'
Delete unknown admin users, unknown scheduled events, must-use plugins you did not install, and any system cron entries you do not recognise. Also check the authorized_keys files of the deploy user and root.
7. Re-scan
A clean scan after cleanup is your confirmation. Run it with the wider package to catch anything the first pass missed:
$ wpsniffer malware-scan --yara-forge extended --match-all /var/www/html
Then check recently changed files once more a day later. If new files appear, the entry point is still open: go back to step 3.
8. Harden and monitor
- Block PHP execution in
wp-content/uploadsand setDISALLOW_FILE_EDIT. - Turn on automatic updates for core, plugins and themes, or put updates on a weekly schedule you actually keep.
- Enforce strong passwords and two-factor authentication for administrators; limit login attempts.
- Give each site its own system user so one compromise cannot write into another site.
- Schedule a nightly scan with an email report so the next attempt is caught within a day.
- If the site was flagged by Google or a blacklist, request a review in Google Search Console once it is clean.