WordPress malware keeps coming back? Here's where it hides
You deleted the infected files, ran a security scan, and it came back clean. By the next morning the fake "verify you are human" box, the spam redirect or the injected script is back. That isn't bad luck: modern WordPress malware is built to survive a clean-up. What you removed was the visible part; the part that puts it back is somewhere else. This guide covers the eight places it hides, why a scanner running inside the site misses them, the order to clean them in, and how to know within an hour if it returns.
Why WordPress malware keeps coming back
A reinfection almost never means the attacker broke in again. It means the clean-up missed a layer. Self-healing malware splits itself up: a small loader that runs on every request, a copy of the payload stored where file scans don't look, and a way back in — an account, a key or a scheduled task. Delete the files you can see and the loader writes them back from the stored copy, often within minutes. Delete the loader but not the account, and the attacker installs it again by hand.
What you see — a fake CAPTCHA, a redirect to a pharmacy site, a skimmer on the checkout — is the payload. It's the least important part to remove, and the only part most clean-ups target. The infection is the combination of the loader, the stored copy and the way back in.
1. A must-use plugin that loads on every request
WordPress runs every PHP file in wp-content/mu-plugins before normal plugins, on every request, and must-use plugins can't be switched off from wp-admin. That makes the folder the favourite place for a loader. A typical one is a few lines with a harmless name (wp-cache-helper.php) that reads an encoded string from the database and evaluates it. There is no malicious code in the file itself for a signature scanner to match.
Look at every file in mu-plugins and ask where it came from. Hosts and a few plugins put files there legitimately; anything that calls eval, base64_decode or gzinflate on data it reads back is not one of them.
2. A copy of the payload in the database
The loader needs something to load. It's kept in wp_options, often as a transient or an option with a system-sounding name, as one long base64 string or as PHP code. Because it's a database row, not a file, file scanners and file restores never touch it — so when you restore the files from a backup, the loader (if it survived) or a scheduled task simply writes the malware back out.
Options that hold PHP code, or one large encoded blob, deserve a look. A few legitimate plugins store code or signatures there (security plugins keep their malware signatures in options), so check before deleting — but a random-looking name holding 40 KB of base64 is not a setting.
3. wp-config.php, .htaccess and .user.ini
These three files run before WordPress does, and they sit outside wp-content, which many scans skip. An auto_prepend_file line in .htaccess or .user.ini makes PHP run a file — often one disguised as an image in uploads — before every single request. An AddHandler line can make .png or .ico files execute as PHP. A pair of RewriteCond lines can send only visitors coming from Google to a spam site, so the site looks normal when you type the address yourself. And wp-config.php can include a file from uploads, or decode a string, on every load.
Wordfence and NinjaFirewall use auto_prepend_file legitimately for their firewalls; anything else prepending a file deserves an explanation.
4. A file hidden among WordPress's own
A PHP file placed in wp-includes/images or wp-admin/includes looks at home among thousands of core files. Reinstalling WordPress doesn't remove it — as WordPress.org's own hacked-site FAQ points out, installers overwrite the files they ship, and hacks usually add new ones. WordPress publishes a checksum for every file it ships, so a core-checksum check catches a modified core file; an added one is in no checksum list, so it has to be looked for separately.
Nothing but WordPress belongs in wp-admin and wp-includes. Any PHP file there that WordPress doesn't ship is a backdoor until proven otherwise.
5. The plugins folder: dropped, hidden, or switched off
Three moves happen here. The attacker copies a plugin in by file instead of installing it, so no install ever appears in any log. They hide it from the Plugins screen with the all_plugins filter, so it never shows in the list you check. And they rename your security plugin's folder — wordfence becomes wordfence.disabled_1712 — which makes WordPress quietly switch it off. No deactivation happens, so no deactivation alert fires, and the security plugin simply stops existing as far as WordPress is concerned.
If your security plugin's dashboard went quiet at the same time the problems started, check its folder name on disk.
6. Accounts and application passwords
The most durable way back in isn't code at all. A new administrator, or an existing customer account quietly given the administrator role, survives every file clean-up. So does an application password: since WordPress 5.6, an administrator can have application passwords that sign in to the REST API directly, without the login form — so two-factor login, login limits and a changed password never touch them.
Check every administrator (Users → filter by Administrator) and, on each one's profile, the Application Passwords section. Revoke anything you didn't create.
7. Scheduled tasks and REST routes
WP-Cron lets code run on a timer. A loader that registers a scheduled task can re-download or re-write the malware every hour, long after you've deleted the files it wrote. A REST route does the same on demand: a public endpoint the attacker calls to push a new copy. Both look like any other hook — what gives them away is where their code lives: evaluated from a string, created at runtime, or sitting in the uploads folder, rather than in a plugin you installed.
Plugins such as WP Crontrol list the scheduled tasks; the question for each unfamiliar one is which file defines it.
8. Your visitors' browsers
Two things live on the visitor's side. A service worker is a script the browser installs for a site and keeps running between visits; it sees every request the site makes and can answer them itself. One registered by injected code stays in your visitors' browsers after the site is cleaned — the one place a server clean-up can't reach. And ClickFix, the fake "verify you are human" box that tells visitors to press Win+R and paste a command, puts malware on your visitors' computers, not your server — so the harm continues until the injection itself is gone.
Injections like these often skip logged-in administrators, so the site can look perfectly normal to you. Check it in a private window, logged out.
Why a scanner inside the site misses it
A security plugin runs inside the site it's protecting, so malware that controls the site can control what the plugin sees: rename its folder and it's off, hide a plugin from the list and it's not there, keep the payload in the database and there's no file to match. Signature scanning also looks for known malicious code, and a loader that evaluates a string from the database contains none.
That's why Relvato is built the other way round. The Relvato plugin only reports what is on the site — fingerprints, names and places, never contents — and the baseline you approved and the verdict live on Relvato's servers, where malware on your site can't edit them. The checks that matter most for this kind of infection look at your site the way a visitor does, from outside. And if the plugin itself stops answering — deactivated, deleted or blocked — Relvato notices within five minutes and flags the site as critical in your notifications and email digest.
Clean it in this order, or it rewrites itself
Order matters because each layer restores the others. Take the site into maintenance mode and take a backup first, even an infected one — you'll want it to work out how they got in.
Then: remove the loader (mu-plugins, anything evaluating code outside a plugin), then the stored payload (the options and transients it reads), then the files (reinstall WordPress core from a fresh download, delete PHP files core doesn't ship, reinstall plugins and themes from source), then the ways back in (unknown administrators, application passwords, scheduled tasks). Put your security plugin's folder name back and run its scan again. Then change every password — WordPress, hosting, SFTP, database — and generate new security keys in wp-config.php so every existing login is signed out. WordPress.org's advice is to change passwords again once you're sure the site is clean, and that's the right moment.
Finally, clear what visitors keep: remove any service worker the site didn't need and the code that registered it.
Watch the next 48 hours
If a layer was missed, you'll know soon: self-healing malware usually comes back within hours, not weeks. A daily check can take a day to see it, so after a clean-up it's worth checking the same places every hour for the next two days.
Relvato does this on its own. When file integrity, theme integrity or the admin roster found something and the next run comes back clean, it starts a reinfection watch: those checks run every hour for 48 hours, and a clean-up during the watch extends it. You can also start one yourself from the site's Security page. Hourly checks are part of plans with scheduled runs.
Where self-healing malware hides, and what finds it
| Where it hides | How it brings the infection back | What Relvato checks |
|---|---|---|
| mu-plugins | A loader runs on every request and can't be switched off in wp-admin | A new file in mu-plugins; malware patterns in it; code it evaluates |
| The database | An encoded copy writes deleted files back | Options holding PHP code or one large encoded blob |
| wp-config.php, .htaccess, .user.ini | A file run before every request; images run as PHP; search visitors redirected | A fingerprint of each file, and the names of malware patterns in them |
| wp-admin and wp-includes | A backdoor beside core's own files survives a core reinstall | PHP files WordPress doesn't ship |
| The plugins folder | A plugin copied in by file and hidden; the security plugin's folder renamed | Plugins not installed through WordPress, hidden plugins, renamed security plugins |
| Accounts | An extra or elevated admin; an application password that skips login and 2FA | New and elevated admins, and every admin's application passwords |
| Scheduled tasks and REST routes | Re-writes the malware on a timer or on request | Code running from where no plugin, theme or WordPress put it |
| Visitors' browsers | A service worker that outlives the clean-up; a fake CAPTCHA (ClickFix) | Service workers the pages install; fake verification prompts |
Questions, answered
Why does my WordPress site keep getting hacked after I clean it?
Usually it isn't hacked again — the clean-up missed a layer. Self-healing malware keeps a loader (often in mu-plugins), a copy of itself in the database, and a way back in (an admin account, an application password or a scheduled task). Remove only the visible files and one of the others puts them back, often within hours.
Does reinstalling WordPress remove malware?
Not all of it. A reinstall overwrites the files WordPress ships, but a PHP file added to wp-includes or wp-admin stays, and nothing in wp-content, wp-config.php, .htaccess or the database is touched. Reinstall core, then look for files WordPress doesn't ship and clean the other layers.
What is the fake 'verify you are human' pop-up on my site?
It's ClickFix: injected code shows a fake CAPTCHA that tells visitors to press Win+R (or open Terminal), paste and press Enter. A script has already put a command on their clipboard, so they install malware on their own computer. It's an infection of your site, not a firewall, and it often hides from logged-in administrators — check the site logged out.
Can my security plugin detect this?
Partly. A security plugin runs inside the site, so malware that controls the site can rename the plugin's folder (switching it off without a deactivation), hide itself from the Plugins screen, and keep its payload in the database where file scans don't look. Keep the security plugin, and add a check whose record and verdict live outside the site.
Do I need to change passwords and security keys?
Yes — after the site is clean, not only when you find the hack. Change WordPress, hosting, SFTP and database passwords, revoke application passwords you didn't create, and generate new security keys in wp-config.php so every existing session is signed out.
How long should I watch the site after a clean-up?
At least 48 hours, and more often than daily: self-healing malware usually returns within hours. Relvato's reinfection watch re-runs the integrity checks every hour for 48 hours after a clean-up, on plans with scheduled runs.