A WordPress site is rarely compromised by a zero-day exploit. It is compromised by the same series of forgotten configurations that have been drifting across the public web for ten years: outdated PHP version, an unpatched plugin, a missing HTTP header, an open xmlrpc.php, an admin login without 2FA. This guide walks through the complete audit of a WordPress site in about 90 minutes, as a visual 10-step checklist. Each step gives the exact copy-paste command, the free tool that automates the verification, and a snapshot of what you should see if the configuration is correct. At the end, your site blocks roughly 90 percent of the automated attacks targeting WordPress in 2026.
The 10-step checklist
- Check PHP and WordPress versions
- Audit installed plugins and their CVEs
- Test HTTP security headers
- Harden
.htaccessor nginx config - Validate SSL/TLS configuration
- Protect
/wp-login.phpand enable 2FA - Decide what to do with
xmlrpc.php - Block user enumeration
- Set up logging and alerting
- Off-site backups and recovery plan
- FAQ
Why this audit, in 90 minutes, now
WordPress powers about 43 percent of the open web in 2026, which makes it the favourite target of automated attack toolkits. The attacker economics are simple: a scraper that probes /xmlrpc.php and /wp-login.php across the whole IPv4 space covers a meaningful chunk of the internet in a week. The defender economics are equally simple: closing the ten well-known holes in this guide forces the attacker to fall back to authenticated plugin exploitation or social engineering, which is roughly one hundred times more expensive per compromise. The 90 minutes invested today quickly save several days of incident response later.
Check PHP and WordPress versions
An outdated PHP or WordPress version is the first automated attack vector. WordPress 6.x and PHP 8.2+ are the minimum baseline in 2026; PHP 7.x is no longer supported and no longer receives security patches.
# SSH into the server php -v wp core version --allow-root wp plugin list --update=available --allow-root
Action if outdated: wp core update then wp plugin update --all. Test on staging before production if you have paid plugins.
Audit installed plugins and their CVEs
Every plugin is a potential entry point. The 2026 rule: zero dormant plugins, zero plugin without an update in 18 months, zero plugin with an unpatched critical CVE.
wp plugin list --field=name | xargs -I {} \
curl -s "https://patchstack.com/api/v1/vulnerabilities?slug={}" | jq '.[] | select(.cvss_score > 7)'
Action: uninstall any “just in case” plugin you do not actively use. For a critical plugin without a fix, look for a maintained alternative or open an urgent ticket.
Test HTTP security headers
HTTP headers are the fastest security gain to deploy: zero functional impact, protection enforced by every modern browser. The 2026 baseline requires six headers.
curl -sI https://your-site.com/ | grep -iE \ "(strict-transport-security|content-security-policy|x-frame-options|x-content-type-options|referrer-policy|permissions-policy)"
Action: add the missing headers at the web server level (nginx add_header or Apache Header always set). CSP is the most complex; deploy first in Content-Security-Policy-Report-Only mode for two weeks.
Harden .htaccess or nginx config
A handful of server-side rules close attack angles that WordPress itself does not cover. Add to .htaccess (Apache) or to the nginx config.
# Apache (.htaccess)
# Block direct access to sensitive files
<FilesMatch "^(wp-config\.php|readme\.html|license\.txt|\.htaccess)$">
Require all denied
</FilesMatch>
# Disable directory listing
Options -Indexes
# Block PHP execution in /uploads/
<Directory "wp-content/uploads">
<FilesMatch "\.php$">
Require all denied
</FilesMatch>
</Directory>
# nginx equivalent
location ~* /(wp-config\.php|readme\.html|license\.txt) {
deny all;
}
location /wp-content/uploads/ {
location ~ \.php$ { deny all; }
}
autoindex off;
Verify: curl https://your-site.com/readme.html must return 403. Same for curl https://your-site.com/wp-content/uploads/test.php.
Validate SSL/TLS configuration
A valid certificate is the entry ticket. A correctly-renewed certificate is the actual operational signal. The 2026 baseline: TLS 1.3 enabled, TLS 1.0 and 1.1 disabled, certificate with more than 30 days remaining, OCSP stapling on.
# Quick command-line check echo | openssl s_client -servername your-site.com -connect your-site.com:443 2>/dev/null \ | openssl x509 -noout -dates -issuer nmap --script ssl-enum-ciphers -p 443 your-site.com
Action: if the grade is below A, copy the nginx or Apache config generated by our TLS Version Selector and deploy it. Set up a 30-day pre-expiry certificate monitor.
Protect /wp-login.php and enable 2FA
The login page is target number one for brute-force attacks. Three measures reduce the surface: server-side rate limiting, mandatory 2FA for admins, and optional renaming of the login URL.
# nginx rate limit on /wp-login.php
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
location = /wp-login.php {
limit_req zone=login burst=2 nodelay;
fastcgi_pass php-fpm;
# ...
}
Verify: attempt ten failed logins in under one minute from a test IP, confirm the 11th is rate-limited. Check that every active admin account has 2FA via wp user list --role=administrator.
Decide what to do with xmlrpc.php
The xmlrpc.php file serves the WordPress mobile app, some Jetpack integrations, and historically pingbacks. If you use neither, block it entirely. Otherwise, restrict it to known IPs.
# nginx: block entirely
location = /xmlrpc.php {
deny all;
}
# Variant: allow Jetpack only
location = /xmlrpc.php {
allow 192.0.64.0/18; # Jetpack
deny all;
}
# Apache (.htaccess) <Files xmlrpc.php> Require all denied </Files>
curl -X POST https://your-site.com/xmlrpc.php must return 403. The Jetpack IP list is published in their official documentation.Block user enumeration
WordPress exposes the user list by default through two endpoints: ?author=N which redirects to user N’s slug, and /wp-json/wp/v2/users which returns the full JSON. Both leaks let an attacker target the brute-force on the right username instead of testing “admin”.
# Block ?author=N (Apache .htaccess)
RewriteEngine On
RewriteCond %{QUERY_STRING} ^author=\d+ [NC]
RewriteRule ^ /? [L,R=301]
# Block /wp-json/wp/v2/users (child theme functions.php)
add_filter('rest_endpoints', function($endpoints) {
if (isset($endpoints['/wp/v2/users'])) {
unset($endpoints['/wp/v2/users']);
}
if (isset($endpoints['/wp/v2/users/(?P<id>[\d]+)'])) {
unset($endpoints['/wp/v2/users/(?P<id>[\d]+)']);
}
return $endpoints;
});
Set up logging and alerting
Without logs, you discover a compromise the day the site is defaced. With logs and alerting, you discover it within the hour following the first abnormal attempt.
- Server logs: nginx/Apache access.log and error.log rotated, kept 90 days minimum.
- Application logs: the WP Activity Log plugin or Wordfence to trace admin actions (plugin modifications, role changes, account creations).
- Uptime and integrity monitoring: SecurityWatch by PeopleAreGeek monitors 5 dimensions (uptime, defacement hash, TLS expiry, header regression, WP version drift) with Slack/Discord webhook alerts.
# Logrotate nginx (Debian/Ubuntu) sudo nano /etc/logrotate.d/nginx # Check "rotate 90" and "compress" sudo logrotate -d /etc/logrotate.d/nginx
Off-site backups and recovery plan
An existing but untested backup is a reassuring fiction. The 3-2-1 rule still holds in 2026: 3 copies, on 2 different media, including 1 off-site. Test restoration every quarter.
# Manual backup via WP-CLI wp db export ~/backup-$(date +%F).sql tar -czf ~/wp-content-$(date +%F).tar.gz wp-content/ # Upload to S3 / Backblaze / OVH Object Storage rclone copy ~/backup-$(date +%F).sql remote:wp-backups/ rclone copy ~/wp-content-$(date +%F).tar.gz remote:wp-backups/
Quarterly test: restore the latest backup to a staging environment, verify everything works, measure recovery time (RTO). A site that takes 6 hours to restore loses that much revenue during a compromise.
Run the audit in one click?
SecuChecker runs the 18 main checks against your URL in under 20 seconds and gives a posture score with the exact fix for every gap. Free, browser-based, non-intrusive.
Recommended audit cadence
One audit is not enough if it is not repeated. The rhythm that works in 2026: full audit (the 10 steps) at launch and every quarter, mini-audit (steps 1, 2, 3 and 5) after every major WordPress update, continuous monitoring via SecurityWatch or equivalent between audits. A well-monitored site has roughly ten times lower incident rate than a site monitored once a year, regardless of the application stack.
Frequently asked questions
How long does a full audit take for a beginner?
Plan 3 to 4 hours for the first audit if you are discovering the environment, then 60 to 90 minutes for subsequent audits once you know your server paths. The longest steps are 3 (headers, sometimes 30 minutes of CSP tuning) and 10 (restoration test, about one hour).
Should I really disable xmlrpc.php?
If you use neither the WordPress mobile app, nor connected-mode Jetpack, nor external publishing software (Microsoft Word, MarsEdit), block it. The security benefit is high (avoids brute-force attacks via the system.multicall method) and the functional cost is zero.
Which security plugins should I use in 2026?
For most sites, Wordfence (free) already covers 80 percent of the needs (application firewall, file scanning, 2FA). For e-commerce or critical sites, add Patchstack or WPScan Premium for real-time CVE monitoring, plus an upstream WAF (Cloudflare Pro or Sucuri).
My host says it “handles security” – is that enough?
A managed host (Kinsta, WP Engine, OVH WordPress) handles the server layer (PHP updates, network firewall, backups), but not the application layer (exposed plugins, admin accounts without 2FA, ?author=N leaking the username). Steps 2, 6, 7 and 8 of this guide remain your responsibility.
How do I know if my site is already compromised?
Common warning signs: abnormal outbound traffic to unknown IPs, new administrator accounts created without your action, hidden pages in /wp-content/uploads/ with pharma or casino content, PHP files in the root with random names. Free diagnostic tools: VirusTotal (URL), Sucuri SiteCheck, and the Wordfence file scan (free).
Should I block all foreign IPs?
No, and it is even counterproductive: Google bot, CDNs, and certain third-party services come from globally distributed IPs. A more effective approach is to block only IPs that fail three times on /wp-login.php via fail2ban or the Wordfence application firewall; the result protects more while inconveniencing less.













