Every WordPress compromise I have triaged in the last five years falls into one of three buckets: stolen or reused admin credentials, an unpatched plugin or theme vulnerability, or a malicious file uploaded through a form that should never have accepted PHP. Almost none came from someone "hacking WordPress core." Core WordPress, despite running roughly 43% of the web, has a remarkably clean track record. Attackers are not breaking down the front door. They are walking in through the side window you forgot to close.
That reframes how to think about hardening. You are not defending against nation-state zero-days. You are defending against opportunistic, automated bots running through lists of known plugin CVEs, breached password dumps, and badly configured upload directories. The fix is a series of small, boring, well-documented controls applied consistently. Most sites I clean up after a compromise had two or three in place. Hardened sites have all thirty.
The threat model: who actually attacks your WordPress site
The realistic adversaries, in rough order of frequency:
- Credential stuffing bots — armies of compromised IPs trying email/password pairs from public breaches against
/wp-login.phpand the REST API. - Plugin/theme CVE scanners — bots that fingerprint your stack and exploit known unpatched vulnerabilities in plugins like older form builders, file managers, or page builders on a schedule.
- Malicious upload pivots — abuse of forms, media uploaders, or insecure plugin endpoints to drop a PHP web shell into
/wp-content/uploadsand call it directly. - Supply-chain compromise — nulled plugins shipped with backdoors, or rare upstream repository compromises.
- Targeted social engineering — phishing an editor and escalating from there. Rare for blogs, common for media properties.
The 32 controls below are ordered by which vectors they shut down. If you skip steps, skip ones that defend a vector you don’t face — not ones that block the most common attacks.
Authentication: where 60% of compromises start
The single highest-leverage area. Authentication controls cost nothing, take an hour, and stop most automated attacks cold. If you do nothing else, do steps 1 through 7.
1. Enforce strong password policies
WordPress’ built-in strength meter is advisory only. Use Password Policy Manager (free) or Wordfence Login Security to enforce a 14-character minimum with mixed character classes, and force a reset on weak accounts. GPU cracking has made 12 characters borderline.
2. Require 2FA for every admin and editor
Not optional anymore. The Two Factor plugin (maintained by core contributors) is free, supports TOTP and U2F security keys, and adds a prompt to the standard login flow. Wordfence Login Security is the equivalent if you’re already on Wordfence. Mandatory for any role that can edit posts or install plugins. Subscribers can stay password-only.
3. Move /wp-login.php to a non-default URL
This will not stop a determined attacker, but it eliminates 99% of credential-stuffing noise. WPS Hide Login (free) sets a custom login slug. Do not skip step 4 just because you did this — obscurity buys breathing room, not security.
4. Limit login attempts
Limit Login Attempts Reloaded is free and lightweight. Configure four attempts per 20 minutes, then a 24-hour lockout. The premium tier adds a cloud blocklist that’s useful but not essential.
5. Use Application Passwords for the REST API
Built into WordPress 5.6+. Generate a per-application token rather than hand your real password to a mobile app, integration, or backup script. Revoke per-application without changing your main credential.
6. Disable XML-RPC if you don’t use it
XML-RPC powers the legacy mobile app, Jetpack, and a few integrations. It is also a brute-force amplifier — a single request can attempt dozens of passwords. If you don’t need it:
add_filter('xmlrpc_enabled', '__return_false');
Or block it at the web server with an NGINX location rule or Apache deny directive. If you re-enable it later for "just one integration," you have undone this protection.
7. Don’t use "admin" as a username
Half of all credential-stuffing traffic targets the literal username admin. If your administrator account is named that, you have donated half the work. Create a new admin with a non-obvious name, transfer post ownership, delete the old one. Also check that /wp-json/wp/v2/users doesn’t leak usernames; restrict the endpoint or strip it.
File system and permissions: locking down what attackers can write
If credentials are how attackers get in, the file system is where they pivot. A web shell dropped into /wp-content/uploads is the most common post-compromise artifact I see.
8. Restrict wp-config.php to mode 600
It contains your database credentials and salts. Only the PHP process needs to read it:
chmod 600 wp-config.php
If shared-user hosting breaks at 600, fall back to 640 with the web server group.
9. Standardize file and directory permissions
Files at 644, directories at 755. From the WordPress root:
find . -type f -exec chmod 644 {} \;
find . -type d -exec chmod 755 {} \;
Never use 777. If a plugin requires it, find a different plugin.
10. Disable the in-admin code editor
In wp-config.php:
define('DISALLOW_FILE_EDIT', true);
Removes the Theme and Plugin File Editor screens. An attacker who steals an admin session can no longer paste a backdoor into functions.php through the UI.
11. Disable plugin and theme installation in production
For sites with a staging environment, lock production read-only:
define('DISALLOW_FILE_MODS', true);
Updates have to run through deploys after this. Skip on small single-site blogs where the owner updates from the dashboard — friction isn’t worth it.
12. Move wp-config.php above the webroot
WordPress looks one directory up if it doesn’t find wp-config.php in the root. On hosting that exposes a directory above public_html, move it. Protects against misconfigurations that serve PHP files as plain text.
13. Block PHP execution in /wp-content/uploads
The single most important file system control. Uploads should hold images and PDFs, never executable code. For Apache, drop this .htaccess in wp-content/uploads:
<FilesMatch "\.(php|phtml|php5|php7|php8|phar)$">
Require all denied
</FilesMatch>
For NGINX, in your server block:
location ~* /wp-content/uploads/.*\.(php|phtml|phar)$ {
deny all;
}
Most "WordPress hacked" calls I get involve a web shell this rule would have neutralized.
14. Define WP_CONTENT_DIR if you’ve relocated wp-content
If you’ve moved wp-content, define WP_CONTENT_DIR and WP_CONTENT_URL explicitly so plugins don’t guess into a writable, web-accessible spot.
Server and transport: hardening the perimeter
Application-layer controls are useless if the network in front is wide open. Mostly one-time server config that pays off forever.
15. Force HTTPS with HSTS
Free Let’s Encrypt certificates are universal. After redirecting HTTP to HTTPS, send:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Browsers refuse HTTP for two years. Submit to the HSTS preload list once you’re confident no subdomain needs HTTP.
16. Run a current PHP version
In 2026, PHP 8.2 or newer. PHP 7.x and 8.0/8.1 are all end-of-life. Outdated PHP is a top reason WordPress sites stay vulnerable to bugs patched everywhere else.
17. Add the security header bundle
Minimum set, via web server config or the HTTP Headers plugin:
X-Frame-Options: SAMEORIGIN(or CSP’sframe-ancestors)X-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-originContent-Security-Policy— start withdefault-src 'self'plus explicit allowlists for CDN, fonts, analyticsPermissions-Policydenying camera, microphone, geolocation unless used
Don’t copy a CSP from a guide and ship it. Test in Content-Security-Policy-Report-Only mode for two weeks, fix what breaks, then enforce.
18. Put a WAF in front
Cloudflare’s free tier blocks roughly 80% of automated attacks before they hit your origin. Pro ($20/month) adds the WAF managed ruleset that catches OWASP top-ten patterns and known CVE exploitation attempts. For commerce, Cloudflare Business or Sucuri’s WAF is worth the upgrade.
19. Block direct PHP access at the server level
Same as step 13, but enforced in the web server config rather than .htaccess, which a compromised plugin can overwrite. Belt and suspenders.
20. Disable directory listing
Apache: Options -Indexes in root .htaccess. NGINX: autoindex off; (default). Without this, attackers fingerprint your stack by browsing /wp-content/plugins/.
Plugins and themes: the largest attack surface
Every plugin is foreign code running with full database access. Treat the plugin directory like a dependency tree, not a feature checklist.
21. Vetting checklist before installing anything
Before you click install:
- Last updated within 6 months (12 for a tiny utility)
- At least 10,000 active installs, or audit the source yourself
- Compatible with your current WordPress version — not just "compatible up to" two versions back
- Responsive support — check the forum tab for recent developer replies
- Search Patchstack or WPScan — any unpatched CVEs are a hard pass
- Sudden negative reviews after an ownership change are a red flag (saw this with several popular plugins in 2024)
22. Delete deactivated plugins and themes
Deactivated files are still on disk. Several CVEs have allowed direct access that runs the code anyway. Keep one fallback theme (the current default), delete the rest. Same for plugins.
23. Enable auto-updates for trusted plugins
Built into WordPress 5.5+. Turn on for security-critical plugins (Wordfence, Akismet, form plugins) and your active theme. Leave off for plugins where a breaking change would take down the site — page builders, e-commerce — and pair those with staging.
24. Subscribe to a vulnerability monitoring service
Patchstack (free tier covers email alerts) and Wordfence both publish CVE feeds. Patchstack’s database is the most comprehensive in 2026; Wordfence’s WAF rules ship faster for popular plugins. Pick one and read the alerts.
25. Never install nulled or cracked plugins
The "free" version of a $99 premium plugin from a sketchy site almost always contains a backdoor. I have personally cleaned three sites where the entry point was a nulled premium SEO plugin. The "free" version cost two weeks of clean-up and a Google blocklisting.
Database and backups: you will need these
26. Change the default wp_ table prefix on new installs
Set a custom prefix like wp_a7k3_ in wp-config.php before initial install. Blocks naive SQLi payloads hard-coding wp_users. Not a substitute for fixing the bug, but raises the bar on automated exploitation. Skip on existing sites — renaming on a live site is risky and the gain is marginal.
27. Daily off-site backups
UpdraftPlus with S3 or Backblaze B2 is the price/performance winner. BlogVault ($89/year) is the no-thinking-required option for non-technical owners. Hosting-level backups from Kinsta or WP Engine are fine as a primary, but do not count as off-site — if your account is compromised or terminated, those backups go with it.
28. Test a restore once per quarter
An untested backup is not a backup. Spin up staging, restore, confirm the site loads. I watched a small e-commerce site lose three days of orders because their nightly backup had been silently failing for six weeks and nobody checked the email.
29. Disable database error display in production
In wp-config.php:
define('WP_DEBUG', false);
define('WP_DEBUG_DISPLAY', false);
define('WP_DEBUG_LOG', true);
Errors go to a log file, not to a public page that leaks query structure.
Monitoring and incident response
The first categories prevent compromise. This one assumes prevention failed and you need to respond well.
30. File integrity monitoring
Wordfence’s free tier scans core, plugin, and theme files against the official .org repository and flags modifications. MalCare does the same off-server with a lighter footprint. Run scans daily, not weekly.
31. Audit log of all admin actions
WP Activity Log records every login, post edit, plugin install, role change, and option update. Free tier covers most needs. Without it, incident response is guesswork.
32. Have an incident response plan
The step everyone skips and regrets later. The five things to do in the first hour of a confirmed compromise:
- Isolate — maintenance page or take the site offline. Cloudflare "Under Attack" mode buys time. Do not delete anything yet.
- Snapshot — full filesystem and database backup of the compromised state. You need it for forensics.
- Rotate everything — admin passwords, the DB password in
wp-config.php, all WordPress salts (regenerate from the official secret-key service), API tokens, SFTP/SSH credentials. - Scan and clean — restore from a known-clean backup if available. Otherwise run Wordfence or Sucuri scanners, inspect
wp-content/uploadsfor stray PHP files, diff core against a fresh download. - Root-cause — before bringing the site back, figure out how they got in. Without this step, you will be cleaning the same compromise next week. Check the audit log, access logs, and modification dates.
Essential vs overkill, by site size. A personal blog does not need every step here, and a member-area or e-commerce site needs every step plus more.
- Personal blog or hobby site: Do steps 1, 2, 4, 5, 8, 9, 10, 13, 15, 16, 18, 22, 23, 25, 27, 30. That’s 16 controls and an afternoon’s work.
- Business site or content publication: Add steps 3, 6, 11, 17, 20, 21, 24, 28, 29, 31, 32. Now you’re at 27 controls and a meaningful security posture.
- E-commerce, membership, or any site holding customer data: All 32, plus a paid WAF (Cloudflare Pro or Sucuri), a managed host with isolation between sites, and a security review of any custom code that touches PII or payments.
Security plugin comparison: Wordfence, Sucuri, Patchstack, MalCare
The four names that come up in every "best WordPress security plugin" thread, with the actual tradeoffs.
| Plugin | Type | Pricing (2026) | Site speed impact | Best for |
|---|---|---|---|---|
| Wordfence | WAF + scanner + login security | Free; Premium $119/yr | Moderate — runs scans on your server | All-in-one for sites that can spare the server resources |
| Sucuri | Cloud WAF + scanner + cleanup service | $200–$500/yr | Low — WAF runs at the edge | Sites that have been compromised before, or run on shared hosting |
| Patchstack | Vulnerability database + virtual patches | Free; Plus $89/yr | Negligible | Sites with many plugins where CVE monitoring is the priority |
| MalCare | Off-server scanner + cleaner + firewall | $99–$249/yr | Very low — scans run on MalCare infrastructure | Resource-constrained sites or shared hosting |
My typical recommendation: Wordfence free for most blogs, Patchstack Plus alongside it for serious vulnerability monitoring, and Sucuri for any site that has been compromised before or handles money. Don’t install all four. Multiple security plugins fight each other and degrade performance without adding much defense.
The best security plugin is the one you actually configure. A free plugin set up correctly beats a $200/year suite that nobody touched after install.
A real example: the 2024-2025 caching plugin pattern
Across 2024 and into 2025, the WordPress security community tracked a recurring pattern in popular caching and optimization plugins: privilege escalation flaws where unauthenticated requests to plugin endpoints could modify site options, including the default user role at registration. Attackers set the default role to administrator, registered a normal account, and instantly had full admin access — no password brute force required.
Hardening above would have stopped this in three places. A WAF (step 18) would have blocked the malformed plugin endpoint requests — both Wordfence and Patchstack pushed virtual patches within hours of disclosure. Vulnerability monitoring (step 24) would have alerted owners before the public exploit dropped. And the audit log (step 31) would have shown the role change happening minutes before a new "admin" account appeared. None of this requires custom code or expensive products. It requires controls installed and looked at occasionally.
Security theater: things that look like hardening but aren’t
- "100+ security checks!" plugins as a substitute for configuration. A plugin that shows a 92/100 score is doing pattern matching. The dashboard makes you feel safe; the unconfigured controls underneath don’t run.
- Hiding
wp-adminwith no 2FA. Step 3 is not a substitute for step 2. An attacker who finds the custom URL (through Referer leaks or a misbehaving plugin) is back to credential stuffing with no second factor in the way. - Daily backups stored on the same server. If the server is compromised or the host terminates your account, those backups go with it. "We have backups" and "we have off-site backups" are different sentences.
- Strong admin password on an account named
admin. You’ve given the attacker half the credential. - Disabling XML-RPC, then re-enabling for "just one integration." Almost every site I’ve audited that did step 6 later flipped it back on. The protection only works if it stays on. If you need XML-RPC, restrict by IP at the web server level instead.
- "We’ll harden it after launch." Hardening costs an afternoon at launch and a week of incident response after a compromise. The economics never favor deferring it.
What to do this week
Day one (90 minutes): Step 7 (rename admin), 1 (rotate to a strong password), 2 (enable 2FA), 4 (limit login attempts), 13 (block PHP in uploads), 27 (verify off-site backups). These six eliminate the most common compromise vectors.
Day two (60 minutes): Steps 8–9 (permissions), 10 (disable file edit), 15–17 (HTTPS, HSTS, headers), 18 (Cloudflare). Transport security and a basic WAF.
Within the first month: Steps 21–22 (audit and delete stale plugins/themes), 24 (Patchstack alerts), 30–31 (file integrity and audit log), 32 (write the incident response plan). You’re now in the top 10% of WordPress sites by security posture, mostly with free software.
WordPress security is not glamorous. It is a small set of boring controls applied consistently and reviewed occasionally. The sites that get compromised aren’t the ones that picked the wrong premium plugin — they’re the ones where nobody looked at the audit log for six months. Set the controls. Read the alerts. Test a restore once a quarter. That’s the whole job.