A WordPress security checklist for small sites
Small WordPress sites rarely get targeted by a person. They get found by automated scanners trying the same weaknesses on millions of sites. That’s good news, because closing those few gaps blocks almost all of it. This WordPress security checklist is the one I ran on this site, in rough order of impact.
Updates and leftovers
- Keep core, plugins and themes updated. Most compromises use a known vulnerability in an outdated plugin.
- Delete what you don’t use. Deactivated plugins can still be exploited if their files are reachable. The same goes for old test installs in subfolders like
/devor/staging, which nobody updates and everyone forgets. - Check for files that shouldn’t exist. There should be no
.phpfiles inwp-content/uploads. WP-CLI’swp core verify-checksumsconfirms core files are untouched.
Logins
- No
adminusername. It’s the first one bots try. WordPress doesn’t let you rename a user in the admin, but you can changeuser_loginin the database or create a new administrator and delete the old one. - Long, unique passwords from a password manager.
- Two-factor authentication. The free Two Factor plugin, built by WordPress contributors, supports authenticator apps and backup codes. Save the backup codes before you need them.
- Hide whether a username exists. WordPress login errors say “unknown username” or “incorrect password”, which confirms valid accounts. A filter makes it generic:
add_filter( 'login_errors', fn() => 'The login details are incorrect.' );
Stop leaking usernames
Even with a custom username, WordPress shows it in a few places:
- The REST API lists users at
/wp-json/wp/v2/users. Remove those routes for visitors who aren’t logged in (the block editor still needs them):
add_filter( 'rest_endpoints', function ( $endpoints ) {
if ( is_user_logged_in() ) {
return $endpoints;
}
foreach ( array_keys( $endpoints ) as $route ) {
if ( str_starts_with( $route, '/wp/v2/users' ) ) {
unset( $endpoints[ $route ] );
}
}
return $endpoints;
} );
- Author archive URLs use
user_nicename. Set it to something that isn’t the login name.
Server settings
In wp-config.php, turn off the theme and plugin editor so a stolen login can’t edit PHP:
define( 'DISALLOW_FILE_EDIT', true );
In .htaccess, add security headers:
Header always set Strict-Transport-Security "max-age=31536000"
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"
Also block files that reveal versions and paths: readme.html, license.txt, plugin readme.txt files and stray error_log files. Block XML-RPC if nothing uses it; many hosts already do.
Backups
Security also means recovery. Keep automatic backups of the files and database, store copies off the server, and test a restore at least once. A backup you’ve never restored is a hope, not a plan.
In your code
If you write themes or plugins, escape everything you output and check capabilities and nonces on every action. I covered the output side in escaping output in WordPress themes.