Most hacked WordPress sites are not victims of clever, targeted attacks. They are caught by automated bots looking for a known plugin vulnerability, a reused password or an exposed backup file. This WordPress security hardening checklist closes those easy doors one by one, with the exact code and server rules to use, so that your site is no longer the low-hanging fruit.
Hardening is layers. No single plugin or setting makes a site “secure”, but each item below removes a real, commonly exploited weakness.
1. Keep WordPress, plugins, themes and PHP updated
Outdated and abandoned plugins are the most common way into a WordPress site. Updates come first, before anything else on this list.
- Enable automatic minor core updates (the default) and consider auto-updates for well-maintained plugins.
- Delete deactivated plugins and unused themes. Deactivated code is still on the server and can still be reached directly.
- Replace plugins that have not been updated for a long time or were closed on WordPress.org.
- Subscribe to a vulnerability feed, such as Wordfence Intelligence, Patchstack or WPScan, so you hear about a vulnerable plugin the day it is disclosed.
If you have WP-CLI access, a quick audit takes seconds:
wp core verify-checksums
wp plugin verify-checksums --all
wp plugin list --update=available
wp theme list --status=inactive
wp user list --role=administrator --fields=ID,user_login,user_email
Checksum verification compares your core and WordPress.org plugin files with the official versions and flags anything that was modified, which is a fast way to spot injected code.
Run a supported PHP version
PHP versions that have reached end of life no longer receive security fixes. Check php.net’s supported versions page and run a version that is still supported, ideally the newest one your plugins are compatible with. A PHP upgrade also makes the site faster, as I cover in the WordPress Core Web Vitals checklist.
2. Lock down wp-config.php, error output and file permissions
A few constants in wp-config.php remove whole categories of risk:
// Disable the theme and plugin file editor in wp-admin.
define( 'DISALLOW_FILE_EDIT', true );
// Optional: also block installing/updating code from wp-admin.
// Only use this if you deploy updates via Git, Composer or WP-CLI.
// define( 'DISALLOW_FILE_MODS', true );
// Never show PHP errors to visitors on production.
define( 'WP_DEBUG', false );
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', '0' );
// If you need logs, write them outside the web root.
// define( 'WP_DEBUG_LOG', '/home/user/logs/wp-debug.log' );
define( 'FORCE_SSL_ADMIN', true );
Displayed errors leak file paths, plugin names and sometimes database details. Make sure your authentication keys and salts are unique (generate them from the WordPress.org secret-key service), and rotate them after any suspected breach. Rotating them logs everyone out.
Set correct file permissions
A common safe baseline on Linux hosting is 755 for directories, 644 for files and a tighter mode for wp-config.php:
cd /var/www/example.com/public_html
find . -type d -exec chmod 755 {} ;
find . -type f -exec chmod 644 {} ;
chmod 640 wp-config.php
The right mode for wp-config.php (640, 600, 440 or 400) depends on which user PHP runs as, so confirm the site still loads afterwards. Never use 777. If a plugin asks for it, that is a reason to distrust the plugin.
3. Disable XML-RPC and block user enumeration
XML-RPC
xmlrpc.php is a common target for brute-force login attempts because one request can try many passwords. Most modern sites don’t need it. The xmlrpc_enabled filter only disables authenticated methods, so the cleaner fix is to block the file at the server or WAF level (see the rules below). One caveat: Jetpack and some older mobile and publishing apps still rely on XML-RPC, so check before blocking it.
User enumeration
Bots collect usernames before they brute-force passwords. WordPress exposes them in a few places: the REST API users endpoint, ?author=1 redirects and the users sitemap. This snippet closes all three for logged-out visitors while keeping the block editor working:
// Hide REST API user endpoints from visitors who are not logged in.
add_filter( 'rest_endpoints', function ( $endpoints ) {
if ( ! is_user_logged_in() ) {
unset( $endpoints['/wp/v2/users'] );
unset( $endpoints['/wp/v2/users/(?P<id>[d]+)'] );
}
return $endpoints;
} );
// Stop ?author=N from redirecting to /author/username/.
add_action( 'template_redirect', function () {
if ( isset( $_GET['author'] ) && ! is_admin() ) {
wp_safe_redirect( home_url( '/' ), 301 );
exit;
}
}, 1 );
// Remove the users sitemap (wp-sitemap-users-1.xml).
add_filter( 'wp_sitemaps_add_provider', function ( $provider, $name ) {
return 'users' === $name ? false : $provider;
}, 10, 2 );
Priority 1 matters for the redirect, because WordPress’s own canonical redirect also runs on template_redirect. Author archive URLs still contain the user’s “nicename”, which defaults to a sanitised copy of the login name. If you show author archives, change it (for example wp user update 2 --user_nicename=jane-editor) and set a public display name that differs from the login.
4. Block access to sensitive files and PHP in uploads
Bots regularly scan for leftover files: error_log, debug.log, database dumps, .zip backups and readme.html, which reveals that the site runs WordPress and invites version probing. On Apache, add rules like these to the root .htaccess:
# Block sensitive files by name
<FilesMatch "^(wp-config.php|readme.html|license.txt|xmlrpc.php|error_log|debug.log|.user.ini|php.ini)$">
Require all denied
</FilesMatch>
# Block backups, dumps and logs by extension
<FilesMatch ".(sql|sql.gz|bak|old|orig|swp|log|tar|tgz)$">
Require all denied
</FilesMatch>
# Don't list directory contents
Options -Indexes
And in wp-content/uploads/.htaccess, stop uploaded files from ever being executed as PHP:
<FilesMatch ".(?i:php|phtml|phar)$">
Require all denied
</FilesMatch>
On Nginx, the equivalent goes in the server block:
location ~* /wp-content/uploads/.*.(php|phtml|phar)$ { deny all; }
location ~* ^/(readme.html|license.txt|xmlrpc.php|wp-config.php)$ { deny all; }
location ~* .(sql|bak|old|log|swp)$ { deny all; }
location ~ /.(?!well-known) { deny all; }
Better still, never store backups inside the web root in the first place.
5. Put a WAF in front of the site
A web application firewall stops many attacks before they reach PHP. On Cloudflare, the managed rulesets plus a few custom rules go a long way:
- Block requests where
http.request.uri.path eq "/xmlrpc.php". - Managed Challenge requests to
/wp-login.php, or allow only your own IP addresses to reach/wp-admin/if your team has fixed IPs. Keep/wp-admin/admin-ajax.phpreachable, because front-end features use it. - Rate limit POST requests to
/wp-login.php.
Application-level firewalls such as Wordfence or a host’s built-in WAF add malware scanning and login protection on top. Use one firewall plugin, not several, since overlapping plugins slow the site and conflict.
6. Full (strict) SSL behind a CDN, HSTS and security headers
If you use Cloudflare, set SSL/TLS mode to Full (strict). “Flexible” mode encrypts traffic only between the visitor and Cloudflare. The connection to your server stays plain HTTP, and it commonly causes redirect loops with WordPress. Full (strict) requires a valid certificate on the origin, which can be a free Cloudflare Origin CA certificate or Let’s Encrypt.
Once the whole site works over HTTPS, add HSTS and other security headers. On Apache with mod_headers:
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
</IfModule>
Start HSTS with a short max-age and only add includeSubDomains if every subdomain supports HTTPS. Don’t add the preload directive unless you understand that removal from browser preload lists is slow. A Content Security Policy is the strongest header but the hardest to get right on WordPress, so roll it out as Content-Security-Policy-Report-Only first and tighten it from the reports.
7. Least-privilege accounts and two-factor authentication
- Give people the lowest role that lets them do their job. Content writers are Authors or Editors, not Administrators.
- Remove accounts for former staff, agencies and freelancers when the work ends.
- Require two-factor authentication for every account with publishing or admin rights. The free Two-Factor plugin maintained by WordPress contributors or your security plugin’s 2FA feature both work.
- Use a password manager and unique, long passwords. Use Application Passwords for integrations instead of sharing a real login.
- Give the database user only the privileges WordPress needs, and use a separate database user per site.
8. Backups you have actually restored
Backups are your last line of defence, and an untested backup is a hope, not a plan.
- Back up files and the database automatically, at a frequency that matches how often content changes.
- Store copies off the server, ideally with a provider that keeps versioned or immutable copies, so an attacker with server access cannot delete them.
- Restore a backup to a staging site regularly to prove it works.
WordPress security hardening checklist: key takeaways
- Update everything, delete unused plugins and themes, and watch a vulnerability feed.
- Run a supported PHP version.
- Disable the file editor and hide error output on production.
- Use 755/644 permissions and tighten
wp-config.php. - Block XML-RPC and user enumeration.
- Deny access to logs, backups, readme files and PHP in uploads.
- Put a WAF in front, use Full (strict) SSL, HSTS and security headers.
- Enforce least privilege and 2FA.
- Keep off-site backups and test restores.
If your site uses custom code, the same discipline applies there: escape output, sanitise input and check capabilities. My guide to custom post types and ACF shows how I handle that in a custom theme.
Frequently asked questions
Do I need a security plugin if I follow this WordPress security hardening checklist?
Not strictly, but a reputable security plugin adds malware scanning, file-change alerts and login protection that are hard to replicate by hand. It complements server-level hardening; it doesn’t replace updates and backups.
Should I change the default wp_ table prefix?
It does little harm on a new install, but it is not real protection, because an attacker who can run SQL queries can read the table names anyway. Spend that effort on updates, a WAF and 2FA instead.
Is it safe to disable XML-RPC in WordPress?
For most sites, yes. Check first whether you use Jetpack, an older WordPress mobile app or a remote publishing tool, because some of these still depend on XML-RPC.
Is hiding the wp-admin login URL enough to stop attacks?
No. Changing the login URL cuts down noise from bots, but it does nothing against vulnerable plugins, which are the main cause of compromises. Treat it as a minor extra, not a defence.
If you want a WordPress site audited and hardened, or cleaned up after a compromise, get in touch and I’ll review your setup.