# guide

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 that wp core verify-checksums also flags, anything with eval of 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_NAME for 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. Then wp core verify-checksums should 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 .htaccess you did not create.
  • wp-config.php, .htaccess, mu-plugins: read them line by line; checksums do not cover them.
  • Database: check siteurl and home in wp_options, look for <script in posts and options, and remove unknown entries in active_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/uploads and set DISALLOW_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.