How to Fix a Hacked WordPress Site
Cleaning malware is only half the job. Contain the site, replace core and extensions with clean copies, close the entry point, and rotate every credential before going public again.
First step: take the site offline or behind maintenance mode before cleaning. A compromised site that stays public keeps serving malware to visitors, keeps sending spam, and accumulates search engine penalties while you work.
Work in this order: contain, assess, clean, close the hole, rotate credentials, then restore visibility.
1. Contain
- Put the site into maintenance mode, or have the host block public traffic.
- Take a full copy of the current state — files and database — before changing anything. It is evidence, and it is the only way back if cleaning goes wrong. Label it clearly as infected.
- Note what you observed: the redirect target, the injected content, the warning Google showed.
- If it is a store, check for fraudulent orders and pause payment processing if anything looks manipulated.
2. Confirm the compromise and its scope
Symptoms map to common attack types:
| Symptom | Typical cause |
|---|---|
| Site redirects to another domain for new visitors only | Injected JavaScript or .htaccess rule, often cookie-aware |
| Spam pages indexed under your domain | SEO spam injection, usually in uploads or a fake plugin |
| Unknown administrator accounts | Database-level access or a privilege escalation flaw |
| Pharma text visible to Google but not to you | Cloaked injection keyed on user agent |
| Host suspended the account for outbound spam | Mailer script uploaded somewhere writable |
| Files change back after you clean them | An active backdoor is still running |
Check whether other sites share the same hosting account. Cross-site infection through a shared directory is common, and cleaning one site while another stays infected achieves nothing.
3. Find the malicious files
Compare against known-good copies rather than trying to read every file:
# Modified in the last 14 days, excluding uploads noise.
find . -type f -name '*.php' -mtime -14 -not -path './wp-content/uploads/*'
# PHP files where PHP has no business being.
find ./wp-content/uploads -type f -name '*.php'
# Common obfuscation markers, as a starting point only.
grep -rEl "eval\(|base64_decode\(|gzinflate\(|str_rot13\(" wp-content --include='*.php'
Treat the results as leads, not verdicts. Legitimate code occasionally uses these functions; malware almost always does, usually alongside long encoded strings.
Any PHP file inside wp-content/uploads/ is suspicious by default — nothing legitimate needs to execute there.
4. Replace rather than clean, where you can
The reliable approach is replacement:
- Core. Delete
wp-adminandwp-includesentirely, then reinstall from a fresh download of the same WordPress version. Never merge — delete first, so injected files do not survive. - Plugins and themes. Delete every one and reinstall clean copies from the repository or the vendor. For anything you cannot get a clean copy of, remove it.
- Uploads. Do not delete this folder. Remove executable files from it and keep the media.
- Root files. Compare
index.php,wp-config.phpand.htaccessagainst reference copies. Injections at the top ofindex.phpand in.htaccessredirect rules are classic.
Then check the database for injected content: unexpected administrator users, injected script tags in wp_posts, and suspicious entries in wp_options — particularly anything scheduled through cron.
5. Find and close the entry point
Cleaning without this step guarantees reinfection. The usual routes:
- an outdated plugin or theme with a known vulnerability;
- a nulled or pirated plugin, which is malware by design;
- weak or reused administrator passwords, or a stolen one;
- stolen FTP or hosting credentials, often from a compromised local machine;
- a vulnerable neighbour site on the same account;
- outdated PHP with unpatched flaws.
Read the access log around the first modification timestamp. A POST request to an unusual path, repeated requests to one plugin's endpoint, or a login from an unexpected country usually stands out.
6. Rotate every credential
Assume everything the site could reach is compromised:
- all administrator and editor passwords, plus a forced logout of active sessions;
- database user password, updated in
wp-config.php; - hosting control panel, SFTP and SSH keys;
- API keys for payment, email and third-party services;
- the WordPress salts in wp-config.php, which invalidates every existing login cookie.
Delete unknown users rather than demoting them, and review the legitimate ones for accounts that should no longer exist.
7. Restore visibility
- Request a review in Google Search Console if the site was flagged, after confirming it is clean.
- Ask the host to lift any suspension once you can show what was fixed.
- Watch the logs and file integrity for a fortnight. Reinfection usually shows up quickly.
- Remove spam pages properly: return 404 or 410 rather than redirecting them somewhere useful.
8. Reduce the odds of a repeat
Keep core, plugins and themes current — safely, on a schedule. Remove extensions you do not use rather than leaving them deactivated. Enforce strong passwords and two-factor authentication for anyone with admin access, and give editors editor accounts rather than administrator ones. Keep off-site backups you have tested restoring, and run integrity monitoring so the next unexpected file change reaches you before it reaches your visitors. A practical baseline is in the WordPress security checklist.
Frequently asked
- Only if the backup predates the compromise and you also close the vulnerability that allowed it. Restoring onto the same outdated plugin gets the site reinfected, often within hours.
- Compare file modification times against a known-good reference, and read access logs around the first suspicious change. The earliest modified file is usually close to the entry point.
- Often yes. If personal data may have been accessed, data protection rules in many jurisdictions require notification within a set period, so take legal advice rather than assuming it does not apply.