Why a Standard WordPress Malware Cleanup Often Isn’t Enough

A real cleanup, as told by Sendy. Different client, different case, same disclaimer. Names, identifying details, and specific timing have been changed. The technical findings and the verification standard are exactly as they happened.

The cleanup report came in on a Friday.

The hosting provider’s security partner had finished the standard remediation. Files cleaned. Database scanned. Patch applied to the plugin that opened the door. Site reported back as clean. The kind of report you read on a Friday afternoon and close the browser feeling like the week ended on a win.

It didn’t end on a win.

By Tuesday the following week, the site had been flagged again. Not by the hosting provider’s scanner. By an enterprise security gateway running on the network of one of the client’s business partners. The business partner’s IT team had gotten an alert that their employees couldn’t reach our client’s site, and the reason their employees couldn’t reach our client’s site was that the scanner was reporting it as serving malicious links.

The cleanup had been thorough. The cleanup had been competent. The cleanup had not been finished. And the case is worth telling because it’s the moment we changed our verification standard for every high-stakes site we manage, and the reason that change exists.

What the first cleanup caught

The original infection was an SEO spam injection through a vulnerability in a performance caching plugin. Common attack pattern. Well-understood. The kind of thing standard cleanups are designed for and usually wrap up without recurrence.

The hosting provider’s security partner ran the standard remediation:

WordPress core files inspected and any modifications reverted to known-good copies. Theme files checked against the version in source control. Plugin files compared to the official versions from the WordPress repository. Database scanned for malicious content in posts, options, and user meta. The vulnerable plugin patched to its latest version. A final scan run to confirm no remaining indicators.

The scan came back clean. The report was honest. By the standards the cleanup was operating against, the work was done.

The problem is that the standards the cleanup was operating against were not the standards the attack was operating against. And on a high-stakes site, that gap matters.

What the first cleanup missed

Here’s where it gets interesting, and where the technical part of the post earns its place.

The malware on the site existed in three locations. The first was the obvious one: injected content in WordPress core and theme files. The standard cleanup found it and reverted it. That’s the part that worked.

The second location was the caching plugin’s cache files. Aggressive caching plugins generate pre-rendered, compressed page output and store it in their own filesystem structure, separate from the standard WordPress filesystem. When a request comes in, the cached version gets served before WordPress even starts running. The attacker had embedded the spam links in the cached output. Cleaning the WordPress core files didn’t touch the cache. The cache kept serving the infected versions of the pages until the plugin was removed and the cache was purged entirely.

That alone would have explained the recurring detection. But there was a third location, and this is the one most people have never heard of.

Pre-WordPress execution files.

These are files that load before WordPress’s main bootstrap. Server-level includes, drop-in cache files, files that PHP loads as part of its own execution path before any WordPress code runs. Code injected at this layer runs on every request, regardless of what state WordPress is in. Standard scans miss it because standard scans look at the WordPress filesystem. The pre-WordPress execution path is the broader server include path, and a scanner that’s only looking at WordPress files is not looking there.

This was the persistent vector. The attacker had established code execution upstream of WordPress itself, and any cleanup that only touched WordPress was leaving the persistence mechanism in place. The Friday cleanup had cleaned WordPress. The infection wasn’t only in WordPress.

“We got the all-clear on Friday. On Tuesday morning I’m fielding calls from our business partner’s IT team because their security scanner is blocking our site for their employees. That’s not a technical problem. That’s a relationship problem and a trust problem. We needed someone who could tell us the work was actually finished, not just reported finished.”

SVP / Chief Technology Officer

What “pre-WordPress execution” actually means in plain language

This is the section worth slowing down for, because the phrase shows up in security discussions more than its explanations do.

Think about what happens when a request hits a WordPress site. The web server receives the request. PHP starts up. PHP loads its own configuration files, including any auto-prepend files or drop-in extensions that the server admin has configured. Then PHP starts executing the application code, which in this case is WordPress. WordPress loads its core, then themes, then plugins, then renders the page and sends a response.

Most WordPress security tools start watching at step four: when WordPress itself starts running. They see what WordPress sees. They don’t see what happens before WordPress wakes up.

An attacker who can place code at steps two or three has established a foothold that’s invisible to almost every WordPress-aware security tool on the market. The code runs on every request. It runs even if WordPress is in maintenance mode. It runs even if the WordPress site has been rebuilt from a backup, as long as the rebuild touches only WordPress files. The persistence is in the layer below WordPress, and the cleanup has to reach into that layer to actually finish.

The cleanup we ended up running on this site required reaching into the pre-WordPress execution path, identifying every file the attacker had touched, restoring those files from known-good copies, and verifying the include path with hashes before declaring it clean. It was more work than the standard cleanup by an order of magnitude. And it was the work that the standard cleanup, by its own scope, was never going to do.

The CDN cache angle nobody talks about

There’s one more layer that has to be cleaned, and it’s the one that gets missed even when everything else is handled correctly.

The site sits behind a CDN. CDNs cache page output at edge locations around the world for performance. When the original infection was active, the CDN cached infected versions of the pages and started serving those cached versions to visitors. Cleaning the origin server does not clean the edge cache. The CDN keeps serving the infected versions, sometimes for hours, sometimes for longer, depending on cache TTL settings and which edge locations have hot cache entries.

On any incident involving content served to public visitors, the cleanup is not complete until every layer of cache has been explicitly purged. Origin cache. Plugin cache. CDN cache, across every edge location. Browser caches are harder to control, but for the public-facing cleanup, the CDN purge is the layer that matters and the layer most often forgotten.

The Tuesday detection that flagged the site to the business partner’s scanner was, in part, the CDN serving a cached version of a page that the origin server had already cleaned. Even if every other layer of cleanup had been perfect, the CDN cache alone would have made the site appear infected until it was purged.

The decision: roll back, then rebuild verification

When the scope of the persistence became clear, the response decision was the same one we wrote about in the previous post in this series. Roll back to a verified clean restore point. Not surgical cleanup. Rollback.

The trade-off is the same trade-off: you lose forensic visibility into the exact mechanism, but you gain absolute certainty that the rolled-back state is provably clean. For a financial institution’s customer-facing site, with a business partner actively flagging it as compromised, certainty was the only acceptable outcome. The rollback took roughly an hour. The site was provably clean before lunch.

What took longer, and what mattered more, was rebuilding the verification standard. The incident proved that one tool reporting clean was not enough. The follow-up scans from the hosting provider had reported clean. The site was not clean. The gap between those two facts was the entire reason the incident recurred.

The new standard, which we have run on every high-stakes site we manage since:

Multiple independent scans, from different vendors. Sucuri SiteCheck, Wordfence’s premium scan, Google Transparency Report, VirusTotal, Cloudflare Radar where applicable, and at least one real-world check from a third geography. No single tool is the source of truth. Each one is a data point.

Filesystem verification beyond the WordPress directory. File integrity checks on the pre-WordPress execution path, on PHP-level includes, on the server’s drop-in cache configuration. Not just /wp-content/. Not just /wp-includes/. The broader include path the attacker can reach.

Database verification beyond malware signatures. No new suspicious users. No new cron jobs. No new admin meta. No unexplained options table entries. No new redirects in any plugin that handles them. The database is checked against a known-good baseline, not just scanned for known-bad signatures.

CDN cache purge before declaring clean. Every edge location, every cached path, every variant. The origin is not the whole site. The cache is part of the site for visitors.

Independent third-party audit before close. A scan from a vendor that did not perform the cleanup. This is the standard we hold ourselves to now, and it exists because of this case. A site that has only ever been verified clean by the same vendor that cleaned it has not really been verified clean.

“For an institution our size, ‘looks clean’ isn’t a standard we can operate at. The question I have to answer for our board and our regulators is whether the work was independently verified, not whether the vendor who did the work said the work was done. The standard wpDuo runs now is the one we should have been running all along, and it’s the one I need from any partner with access to our environment.”

Chief Information Security Officer

What changed after this case, for every site we manage

The framing matters here, because the easy version of this post would be the one that throws the original cleanup vendor under the bus. That’s not the story.

The original vendor ran a competent standard cleanup against what was, by 2020 standards, a standard attack. The attack vectors have changed. Spam injection campaigns now routinely use multi-layer persistence, including the pre-WordPress execution path, exactly because they know standard cleanups don’t reach that layer. Caching plugins create attack surface that didn’t exist five years ago. CDN cache poisoning is more common than it used to be. The threats moved faster than the cleanup standard.

What we adopted after this case is not a critique of the original work. It is a recognition that the verification standard has to evolve with the threat. Extra checks are the standard for us now. Multiple vendors. Multiple geographies. Pre-WordPress execution scanning. CDN purge as a required step. Independent audit before close. The cost of those extra checks is some hours of work. The cost of not running them, on a high-stakes site, is what happened on this site: a clean report that wasn’t clean, a business partner flagging the site, and a Tuesday morning conversation nobody wanted to have.

The principle

Verified clean requires multiple independent confirmations. One tool reporting clean is one data point. A second tool from a different vendor is a second data point. A scan from a different geography is a third. The verification standard has to match the threat model, and on a site where the consequences of getting it wrong are regulatory exposure, business partner relationships, or customer trust, the verification standard has to be higher than the standard you’d run on a low-stakes blog.

This is the third post in the series. We’ve covered the architecture that keeps attackers out, the monitoring that catches the ones that get in anyway, and the verification standard that confirms the response is actually finished. The next post in the series moves up one level. Most of the work we’ve described is what we do during an incident. The work that determines whether the incident causes lasting damage, beyond the technical recovery, is what happens in the communication during and after.

What you say, when you say it, who you say it to, and how. That’s next.

Sendy out. ✈️


Frequently Asked Questions

Why does WordPress malware sometimes come back after cleanup?

The most common reasons are incomplete cleanup of caching layers (plugin cache, CDN cache), unaddressed persistence mechanisms outside the WordPress filesystem (such as pre-WordPress execution files or server-level includes), and reliance on a single scan from a single vendor for verification. A cleanup that only addresses WordPress core, themes, and plugins will miss any malware that lives in the broader server execution path or in cached output.

What is “pre-WordPress execution” malware?

Pre-WordPress execution malware is code that runs before WordPress itself starts up. It typically lives in PHP auto-prepend files, server-level drop-in cache files, or other parts of the server’s execution path that load before WordPress’s bootstrap. Standard WordPress security tools don’t scan these locations because they only look at the WordPress filesystem. Cleaning the WordPress files alone leaves this persistence in place, and the infection recurs on the next request that triggers the upstream code.

Is a single scan from Sucuri or Wordfence enough to confirm a site is clean?

For high-stakes sites, no. A single scan from a single tool is one data point. Real verification on a site with regulatory exposure or significant business risk should include multiple independent scans from different vendors, file integrity verification beyond the WordPress directory, database verification against a known-good baseline, CDN cache purge before declaring clean, and an independent third-party audit performed by a vendor different from the one that did the cleanup.

Why is CDN cache purge so important after a WordPress incident?

CDNs cache page output at edge locations and continue serving cached versions even after the origin server has been cleaned. If the cached versions contain malicious content from the time of the infection, the CDN will keep serving infected pages to visitors until the cache is explicitly purged. On any incident involving content served to public visitors, a full CDN purge is a required step before the cleanup is considered complete.

Should I trust the same vendor that cleaned my site to verify it’s clean?

A vendor verifying their own work has an incentive to find that the work was complete. That’s not a comment on integrity, it’s a comment on how verification standards work in any industry. For a high-stakes site, the verification scan should come from a vendor that did not perform the cleanup. The independence is what makes the verification meaningful.


This is part three of a series on running WordPress like enterprise infrastructure. If your site is more important than your current security setup reflects, book a free 30-minute strategy session. No pitch. Just clarity on what’s actually protecting you and what isn’t.

Part one: The Six-Layer WordPress Security Model for Sites That Matter

Part two: We Detected a Live WordPress Attack in Under 10 Minutes. Here’s How.

Comments are closed.

Back to blog