Case study · WordPress incident response

Case study: the WordPress site that kept getting reinfected — and how it stopped.

A production e-commerce site had been cleaned three times by three different parties, and the malicious behavior returned after every cleanup. The fourth engagement treated the recurrence itself as the finding — and ended the cycle by closing what the earlier cleanups had left behind. This anonymized account shows the investigation pattern while withholding client-identifying and attacker-useful details.

Environment
Production WordPress e-commerce site
History
Three prior cleanups by other parties; infection returned each time
Objective
Remove every persistence layer and identify the likely access path
Outcome
Persistence removed, access reset, entry condition addressed, recovery verified

Starting condition

Three cleanups had bought quiet weeks, not recovery

By the time the engagement began, the pattern was well established: a scan or manual cleanup would remove the visible malicious files, the site would behave normally for a while, and then the same class of malicious behavior would reappear. Each round cost the owner cleanup fees, lost sales during the disruption, and — more damaging for an e-commerce business — growing doubt about whether the store could be trusted at all.

Recurrence after cleanup is itself diagnostic. It meant at least one of four things had survived every attempt: a persistence mechanism outside the locations being cleaned, an access path the attacker could reuse, a compromised neighboring component within the same hosting boundary, or a credential that rotation had missed. The engagement was therefore scoped as an incident investigation with the explicit goal of ending the cycle — not as a fourth cleanup.

Importantly, the earlier cleanups were not incompetent; they were incomplete in a specific, common way. Each had focused on the visible file layer — the layer scanners report — while the mechanisms that restored the infection lived elsewhere.

Why the cleanups failed

The patterns that let an infection outlive its removal

Several of the classic cleanup-failure patterns were present in this engagement’s history. They are worth naming because they repeat across almost every reinfection case:

Scan-and-delete only

Flagged files were removed, but components were never rebuilt from trusted sources — so anything the scanner did not recognize stayed exactly where it was.

The database was never inspected

Cleanup stopped at the filesystem. Persistence stored in WordPress records survives every file-level cleanup and theme reinstall untouched.

Partial credential rotation

The WordPress admin password changed; hosting, database, FTP/SFTP, and session-level access did not all change with it — leaving the attacker a way back that made the next “cleanup” irrelevant.

No entry-path investigation

None of the three rounds asked how access was first obtained. The door that admitted the attacker stayed open through all three.

Verification stopped at the homepage

Each round ended when the visible symptom disappeared — before caches expired, scheduled tasks ran, and normal traffic resumed, which is exactly when the infection reloaded.

The investigation

The fourth engagement separated symptom removal from recovery

  1. 01

    Establish scope and history

    Document the observed behavior, the full cleanup history, every access route (WordPress, hosting panel, database, SFTP, deployment), the hosting boundary, and the business constraints on downtime.

  2. 02

    Inventory every persistence layer

    Review each location capable of restoring malicious behavior — files, database records, the active theme, PHP and server configuration, scheduled tasks, and accounts — and give each an explicit trust decision rather than a scan pass.

  3. 03

    Restore trust, not just appearance

    Remove untrusted artifacts, rebuild affected components from known-good sources, reconcile legitimate customizations separately, and rotate every relevant credential and session from a clean device.

  4. 04

    Close the entry condition and verify

    Address the most likely initial-access condition supported by the evidence, apply the agreed hardening, and recheck the environment past the points where earlier cleanups had declared victory.

If this is your site

If your WordPress site keeps getting reinfected, check these five places

The full checklist is in the reinfection guide; these are the layers that most commonly survive a cleanup:

  • The WordPress database — encoded content in options records, especially autoloaded values
  • PHP configuration — .user.ini or php.ini directives that load code before WordPress starts
  • Locations code is not expected — mu-plugins, drop-ins, uploads, and cache directories
  • Identities rather than files — rogue admins, application passwords, surviving sessions, SSH/SFTP keys, deployment tokens
  • Neighboring sites on the same hosting account, which can reinfect a perfectly cleaned site

Outcome

The cycle ended when the last surviving layer was closed

The engagement removed the identified persistence, restored affected components from trusted sources, reset every relevant access path, and addressed the most likely entry condition available within the evidence and scope. Verification then deliberately extended past the milestones where the previous cleanups had stopped — through cache expiry, scheduled execution, and a return to normal traffic — and the recheck confirmed the environment stayed clean.

Just as important for the owner: the result was documented. The report recorded which layers had held the infection, what was rotated and rebuilt, which entry condition was addressed, and what residual dependencies remained — so any future responder starts from evidence instead of repeating the same assumptions a fourth time.

Because public case studies should not provide a roadmap back into a former client environment, exact indicators, filenames, infrastructure, dates, and access paths are intentionally omitted.

Lessons

What other site owners can take from this incident

  • Reinfection after a cleanup is a diagnosis, not bad luck — something specific survived.
  • A clean scan does not prove that every persistence path is gone; only an explicit trust decision per layer does.
  • Restoring a backup is unsafe until the backup and the entry condition are both understood.
  • Credential rotation only counts when it covers WordPress, hosting, database, transfer, deployment, and active sessions together.
  • Multiple sites sharing an account share a containment boundary — one clean site proves little.
  • Cleanup should end with documented hardening and a verification pass that outlasts caches, cron, and cache-refresh cycles.