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:

  1. Credential stuffing bots — armies of compromised IPs trying email/password pairs from public breaches against /wp-login.php and the REST API.
  2. 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.
  3. Malicious upload pivots — abuse of forms, media uploaders, or insecure plugin endpoints to drop a PHP web shell into /wp-content/uploads and call it directly.
  4. Supply-chain compromise — nulled plugins shipped with backdoors, or rare upstream repository compromises.
  5. 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’s frame-ancestors)
  • X-Content-Type-Options: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin
  • Content-Security-Policy — start with default-src 'self' plus explicit allowlists for CDN, fonts, analytics
  • Permissions-Policy denying 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:

  1. Isolate — maintenance page or take the site offline. Cloudflare "Under Attack" mode buys time. Do not delete anything yet.
  2. Snapshot — full filesystem and database backup of the compromised state. You need it for forensics.
  3. 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.
  4. Scan and clean — restore from a known-clean backup if available. Otherwise run Wordfence or Sucuri scanners, inspect wp-content/uploads for stray PHP files, diff core against a fresh download.
  5. 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-admin with 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.