How to remove malware from WordPress, step by step
A practical WordPress malware cleanup guide covering backups, core files, plugins, database checks, hidden access and validation with WP-CLI.
- Guides
- Malware
- Incident response
Removing the file a scanner flagged isn’t always the same as removing the infection. There may be another way back in, a scheduled task that recreates it or a second affected site on the account.
This is the process I use to structure a WordPress cleanup. It’s written for someone who understands SSH, backups and WordPress files. If you’ve only just found the problem, start with what to do when your WordPress site is hacked.
Only use these steps on a site you’re authorized to manage. For a store, a multisite installation or a site holding sensitive information, I’d plan the recovery with the host rather than experiment on production.
Before running commands
Use a trusted device and run commands as the site’s system user, not root. Work in a restricted recovery environment where possible. Stop outbound email and prevent the recovery copy from triggering real payments or scheduled integrations.
WP-CLI isn’t a malware sandbox. Commands that load WordPress can execute compromised code. Even --skip-plugins --skip-themes doesn’t skip must-use plugins, and configuration files or PHP settings can still be involved. Review the environment before running commands, or ask your host to handle that part.
The examples below assume a standard single-site installation. Replace example paths and version values with your own. They aren’t a script to paste and run from top to bottom.
1. Preserve the current files and database
Ask the host for a snapshot and preserve the available logs. If you’re making a manual copy, keep it outside the public web directory.
For example, after confirming these paths and that the incident directory is private:
SITE=/home/siteuser/public_html
CASE=/home/siteuser/incident-copy
mkdir -p "$CASE"
chmod 700 "$CASE"
tar -czf "$CASE/site-files.tar.gz" -C "$SITE" .
wp --path="$SITE" db export "$CASE/database.sql"
chmod 600 "$CASE/site-files.tar.gz" "$CASE/database.sql"
The database export reads site configuration, so use the host’s database export tool instead if that configuration hasn’t been assessed. Don’t place an SQL dump or archive under public_html, even temporarily.
This is an infected evidence copy. Label it, restrict access and don’t restore it as a clean backup. If the site is harming visitors, restrict public access at the host while you work.
2. Secure accounts and review users
Secure hosting and recovery email accounts, SFTP/SSH access and WordPress users from a trusted device. Review SSH keys, WordPress application passwords and active sessions too.
List administrators in the assessed recovery environment:
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
Confirm unfamiliar users with the owner before removing them. Record the details first, and don’t delete associated content blindly.
Changing the database password also requires updating wp-config.php. Plan those changes together. Refresh WordPress security keys to invalidate cookie-based sessions:
wp config shuffle-salts
That doesn’t revoke application passwords. Rotate credentials again after cleanup if they may have remained readable to the attacker.
3. Verify and replace WordPress core
Start with a checksum comparison:
wp core verify-checksums --include-root
This command checks core without loading WordPress. Confirm the expected WordPress version and locale from trusted records if the installed files can’t be trusted.
To replace core with the same official release, set the version and locale explicitly. This example uses placeholders you must replace:
WP_VERSION='REPLACE_WITH_CONFIRMED_VERSION'
WP_LOCALE='REPLACE_WITH_CONFIRMED_LOCALE'
wp core download --version="$WP_VERSION" --locale="$WP_LOCALE" --force --skip-content
wp core verify-checksums --version="$WP_VERSION" --locale="$WP_LOCALE" --include-root
--force overwrites matching files. It doesn’t remove extra files left by an attacker. --skip-content omits bundled themes and plugins; it doesn’t clean your existing wp-content directory. Review extra files separately, including files inside core directories.
Replacing the same release helps separate cleanup from an upgrade. If that release is outdated, move to a supported, patched version and test compatibility before reopening the site. Don’t restore a vulnerable release and leave it exposed.
4. Reinstall plugins and themes from trusted sources
Inventory what’s installed:
wp plugin list
wp theme list
wp plugin verify-checksums --all
Plugin checksums are available for supported WordPress.org releases, not every premium or custom plugin. A missing checksum isn’t proof of malware, and a successful check doesn’t inspect the database or unrelated files.
Compare each component with a fresh package from WordPress.org or the original vendor. Preserve custom changes before replacing directories. Simply overwriting a plugin can leave attacker-added files behind.
Remove unused components after confirming they’re not required. Replace nulled plugins and themes with legitimate copies or alternatives. Check must-use plugins and drop-ins such as object-cache.php too. Some are legitimate host integrations, so don’t delete them based on the filename alone.
5. Look for persistence outside normal plugins
These read-only searches can help narrow the investigation. Run them from the site root:
# PHP files in the uploads directory
find wp-content/uploads -type f -iname '*.php'
# Automatically loaded must-use plugins
ls -la wp-content/mu-plugins
# Recently changed PHP files
find . -type f -iname '*.php' -mtime -14
# PHP configuration that can prepend another file
rg -n --hidden --no-ignore 'auto_prepend_file' -g '.user.ini' -g '.htaccess' -g 'php.ini' .
The final command requires ripgrep. If it’s unavailable, inspect those configuration files through SFTP or your hosting panel.
These are clues, not a malware verdict. Legitimate updates change timestamps, attackers can alter timestamps, and malicious files don’t have to end in .php. Must-use plugins may belong to your host. An absent mu-plugins directory is normal too.
Inspect .htaccess, wp-config.php, PHP configuration and server-level redirects. With Nginx, relevant rules may be in the host’s server configuration rather than .htaccess. Also review hosting scheduled tasks and other sites sharing the account.
6. Review the database and scheduled tasks
Look for unfamiliar posts, injected links, changed site URLs and options that load scripts. A small read-only query can flag posts for review:
PREFIX=$(wp db prefix)
wp db query "SELECT ID, post_title FROM ${PREFIX}posts WHERE post_content LIKE '%<script%';"
wp cron event list
This query isn’t a complete detection method. Legitimate content can contain scripts, and malicious content may use different markup, encoding or another table.
Map unfamiliar cron hooks to their plugins before changing anything. Don’t run bulk SQL replacements on serialized options: you can corrupt the data. Preserve affected rows before making targeted edits.
If the symptom is search spam, also check unexpected sitemaps and generated URLs. My Japanese keyword spam guide covers that recovery path.
7. Close the entry point
Use available logs, component versions and account activity to establish what happened. A vulnerable plugin being present is a lead, not proof that it was the entry point.
Patch or replace vulnerable components, remove unauthorized access and fix unsafe permissions or hosting configuration. If you can’t establish the cause, record that uncertainty. Don’t tell the owner the entry point is closed just because the homepage looks normal.
A clean backup can help, but check its date, inspect it and address the vulnerability before restoring public access. Plan how to retain legitimate orders and changes made since the backup.
8. Validate before reopening
Check the site as a logged-out visitor on desktop and mobile. Test forms, login, scheduled jobs and checkout where relevant, without triggering real charges on a recovery copy.
Recheck core files, users, application passwords, redirects and scheduled tasks. Clear server, plugin and CDN caches once the malicious source is removed. A clean cache can hide infected origin files, and a stale cache can keep serving removed content.
For deleted spam URLs, confirm real HTTP 404 or 410 responses. Don’t redirect everything to the homepage.
In Google Search Console, check Security issues and Manual actions separately. Request the relevant review only after fixing the reported issue. Search and browser warnings don’t disappear on a guaranteed schedule.
9. Keep watching after cleanup
Enable two-factor authentication for privileged accounts, maintain off-server backups and test restorations. Keep WordPress, plugins and themes updated through a process that includes backups and compatibility checks.
Monitor changed files, new accounts, scheduled tasks and hosting alerts. Disabling the dashboard file editor can reduce one route for changing code, but it doesn’t stop an existing backdoor:
define( 'DISALLOW_FILE_EDIT', true );
Add that setting only if it isn’t already defined. File permissions should suit the host’s ownership model; avoid broad recursive changes copied from a generic checklist.
I’d call the cleanup incomplete if the malware keeps returning, another site on the account is still infected or the cause remains unexplained. That’s the point to widen the investigation, not keep deleting the same file.
Want me to take a look?
I offer a free diagnosis based on what I can see from outside your site. Send me the domain and what you’re seeing. Don’t send passwords over WhatsApp. Cleanup is a separate service, with the scope agreed before work starts.
Ask me for a free diagnosis on WhatsApp or see my WordPress security services.