The WordPress Security Checklist We Actually Use on Client Sites
The WordPress security checklist we run on real client sites: uploads PHP blocking, wp-config constants, plugin audits, 2FA, edge rate limiting and restores.
- Plugins, not core, are where almost all WordPress vulnerabilities live. Patchstack’s annual disclosure reports have put plugins in the mid-nineties percent range for years running. Your inventory discipline matters more than your firewall.
- Blocking PHP execution in
wp-content/uploadsis the single highest value five-minute change you can make. It turns most arbitrary file upload bugs from site takeover into a useless file sitting on disk. - WordPress 6.8 moved default password hashing to bcrypt, which means a stolen 2026-era
wp_userstable is far less catastrophic than a 2019 one. It does not help accounts created before the upgrade until those users log in again. - Rotating salts with
wp config shuffle-saltsinvalidates every logged-in session instantly. It is the first command to run during an incident, before you start deleting files. - A backup you have never restored is a hypothesis. Test a full restore to a scratch environment quarterly, and keep at least one copy somewhere the WordPress server has no credentials for.
Start with who is actually attacking you
Most WordPress sites are not targeted. They are swept. Bots crawl for known vulnerable plugin versions, probe /xmlrpc.php and /wp-login.php, and try credential stuffing against whatever usernames they can enumerate. The attacker does not know or care what your business does.
That shapes everything. You are optimising against automation, not against a human who will adapt. The cheap, boring, structural controls (patch currency, upload execution blocking, least privilege) buy you far more than clever obscurity. Hiding your WordPress version number buys you nothing: the scanner fingerprints your plugin assets anyway.
The exception: ecommerce and membership sites with real money moving through them do attract targeted attention, usually card skimming injected into checkout. If that’s you, the monitoring section matters more than the hardening section.

The WordPress security checklist we run before every handover
This is the actual list from our handover doc, trimmed of client-specific items.
- PHP 8.2 or newer, and on a version still receiving security fixes. PHP 8.1 hit end of life in December 2025.
- Core on the latest minor, auto-updates for minors enabled.
- Full plugin and theme inventory, with last-updated date and active install count recorded.
- Anything not updated in 12 months flagged for replacement.
- PHP execution denied in
wp-content/uploadsand any other writable directory. - File editor disabled in wp-admin.
- Administrator accounts counted and justified. Usually two.
- 2FA mandatory on every account with
edit_postsor above. - Default
adminusername absent, author enumeration closed. - XML-RPC off unless Jetpack or the mobile app is genuinely in use.
- Backups running offsite, with a documented restore that someone has actually performed.
- Vulnerability monitoring hooked to a channel a human reads.
- Security headers set at the edge, HSTS included.
- Staging environment that is not publicly indexable and does not share the production database.
Fourteen items. Most take minutes. The two that take real work are the plugin audit and the restore test, and those are the two most people skip.
The file and config layer costs nothing and stops the most
Start here. No ongoing maintenance, pure gain.
Block PHP in uploads. Apache, dropped into wp-content/uploads/.htaccess:
<FilesMatch "\.(?i:php|phtml|php[0-9]|phar|shtml)$">
Require all denied
</FilesMatch>
Nginx, in the server block (note this must come before your generic PHP location or it will never match):
location ~ ^/wp-content/uploads/.\.(php|phtml|php[0-9]|phar)$ {
deny all;
access_log off;
}
Then the wp-config constants. Not all of them, and not blindly:
// No code editing from the dashboard, ever. Zero legitimate use on a maintained site.
define( 'DISALLOWFILEEDIT', true );
// Minor core updates install themselves. Majors stay manual.
define( 'WPAUTOUPDATE_CORE', 'minor' );
define( 'FORCESSLADMIN', true );
// If WP_DEBUG is ever left on in production, at least keep it out of the HTML.
define( 'WPDEBUGDISPLAY', false );
define( 'WPDEBUGLOG', '/var/log/wp/debug.log' ); // outside the webroot
DISALLOWFILEMODS is the one people recommend and then regret. It kills the plugin installer, the updater and the theme installer entirely. On a site where you deploy plugins through Composer and Git, set it. On a site where the client’s marketing lead needs to install a landing page plugin on a Friday, you have just created a support ticket that ends with someone commenting out the constant and never putting it back. Half-enforced hardening is worse than none, because you stop trusting your own config.
Permissions: directories 755, files 644, wp-config.php 640 or 600. The web user should not own the WordPress files it executes, with the exception of wp-content/uploads. If your host insists on 777 anywhere, change hosts.
Logins, sessions and the parts people get wrong
WordPress 6.8 switched default password hashing from the old phpass implementation to bcrypt. Genuinely meaningful. Also widely misunderstood: existing hashes are not rewritten on upgrade. They are rehashed the next time that user successfully logs in. An account that has not logged in since 2024 still has a weak hash sitting in your database.
Force a password reset after you upgrade a legacy site. Then put 2FA on everything with publishing rights.
Close enumeration while you are in there. A must-use plugin at wp-content/mu-plugins/hardening.php is the right home for this, because it cannot be deactivated from the dashboard:
<?php
/**
- Plugin Name: Site Hardening
- Description: Non-negotiable security defaults. Do not deactivate.
*/
addfilter( 'xmlrpcenabled', '__return_false' );
// Stop the X-Pingback header advertising an endpoint we have disabled.
addfilter( 'wpheaders', function ( $headers ) {
unset( $headers['X-Pingback'] );
return $headers;
} );
// The users endpoint hands out every author login. Require auth for it.
addfilter( 'restendpoints', function ( $endpoints ) {
if ( isuserlogged_in() ) {
return $endpoints;
}
unset( $endpoints['/wp/v2/users'] );
unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
return $endpoints;
} );
// Never tell an attacker which half of the credential pair was wrong.
addfilter( 'loginerrors', function () {
return 'Login failed.';
} );
One caveat on the users endpoint: if you have a decoupled front end reading authors over REST, this breaks it. Use an authenticated request with an Application Password instead of reopening the endpoint, and if you are building anything substantial on top of the API, the auth and caching patterns in our piece on custom REST API endpoints in WordPress will save you some rework.
Application Passwords deserve their own note. They are scoped to a user, not a capability, so an Application Password issued to a deploy script for an administrator account is an administrator credential. Create a dedicated user with the minimum role, issue the password to that user, and revoke it when the integration dies.
Plugins are the supply chain, and you are the one accepting the risk
If you only do one thing from this article, make it this. Core WordPress has a competent security team and a fast release cadence. The 60,000 plugins in the directory do not.
Our rules for taking a plugin to production:
- Last update inside six months. Not “tested up to” the current version, actual commits.
- Someone is reachable. A support forum with unanswered threads from 2024 is an abandoned plugin with a marketing page.
- Does it write to the filesystem or accept uploads? If yes, the bar goes up sharply. File upload handlers are where the critical CVEs cluster.
- Count the dependencies it drags in. A form plugin that bundles its own copy of a PDF library is three attack surfaces, not one.
- Could 30 lines in the theme replace it? Very often, yes.
Contact forms are the clearest example. A full form builder plugin gives you a database table of submissions, a file upload handler, an email sender and an admin UI, all to collect a name and a message. On static builds and on sites where we want the submission data out of WordPress entirely, we point the form at WebForms and delete the plugin. No PHP endpoint, no stored submissions to leak.
Run checksum verification as part of your routine. It catches modified core and directory-hosted plugin files, which is exactly the signature of the uploads-directory backdoor:
wp core verify-checksums
wp plugin verify-checksums --all
Inventory with pending updates, straight into your audit doc
wp plugin list --fields=name,version,updateversion,status,autoupdate
Who actually has admin?
wp user list --role=administrator --fields=userlogin,useremail,user_registered
For vulnerability alerting, Patchstack and Wordfence Intelligence both publish feeds. Pick one, wire it to Slack or email, and make sure the alert lands somewhere a human looks at within a day. An unread alert is not monitoring.
Push the blocking to the edge, not into PHP
Rate limiting login attempts inside WordPress means every attempt still boots the entire PHP stack, loads your plugins, hits the database, then declines. Under a credential stuffing run, that is how a “secure” site falls over from its own defences.
Do it upstream: Cloudflare rate limiting rules on /wp-login.php and /xmlrpc.php, fail2ban reading your access log, or your host’s WAF if it has one worth the name. Nginx can handle the simple case natively:
limitreqzone $binaryremoteaddr zone=wplogin:10m rate=20r/m;
location = /wp-login.php {
limit_req zone=wplogin burst=5 nodelay;
include fastcgi_params;
fastcgi_pass php-fpm;
}
Headers belong at the edge too. HSTS, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin and a sane Permissions-Policy are all free.
Content Security Policy: a strict CSP on /wp-admin is not worth your time. The admin is full of inline scripts and third-party plugin assets, and you will spend a week on nonces and still break the block editor. Ship a real CSP on the front end, where you control the output, and leave the admin alone. If your front end is decoupled anyway, this gets dramatically easier, and the trade-offs there are covered in our take on headless WordPress.
Backups, and the restore nobody has tried
Three questions decide whether your backup strategy is real:
- Can the WordPress server delete the backups? If the same credentials that run the site can reach the backup bucket, ransomware deletes both. Use write-only object storage credentials or a pull-based backup from outside.
- How far back do you keep? Site compromises are frequently discovered weeks later. Seven days of retention means your only restore point is already infected. Keep 30 days minimum, with monthly snapshots beyond that.
- When did someone last restore one? Not verified the file exists. Restored it, to a scratch host, and loaded the homepage.
We put the restore test on a calendar invite. It takes an hour and it is the only part of this list that has ever saved a client six figures.
When it has already happened
Order matters, and the instinct to start deleting malicious files first is wrong. Do this:
- Rotate salts.
wp config shuffle-saltslogs out every session immediately, including the attacker’s. - Reset all passwords and revoke every Application Password.
- Snapshot everything before you clean. Files and database. You will need it to work out the entry point.
- Audit for persistence in the places people forget:
wp-content/mu-plugins/, the drop-insobject-cache.phpandadvanced-cache.php, scheduled events (wp cron event list), unexpected administrator rows, and autoloadedwp_optionsentries with base64 in them. - Check the uploads tree for PHP:
find wp-content/uploads -name '.php' -o -name '.phar'. There should be zero results. - Then restore from a clean backup and patch the entry point. Cleaning in place without identifying the vulnerability means you will be doing this again next month.
Frequently Asked Questions
Do I need a security plugin like Wordfence or Sucuri?
Not necessarily, and on well-maintained sites we often skip them. Their main value is the WAF and malware scanning, both of which are better handled at the edge and via checksum verification respectively. Where they do earn their place is on sites you cannot fully control, for example a client who installs plugins without asking, because the virtual patching buys you a window between disclosure and update.
Should I change the wp-admin login URL?
It stops a lot of log noise and almost no determined attacker. Renaming the login page is obscurity, not a control, and some plugins break when they hardcode wp-login.php. If quieter logs help you spot real signals, do it, but do not count it as a hardening step and never let it substitute for 2FA and rate limiting.
Is changing the database table prefix worth doing?
On a new install it costs nothing, so sure. On an existing site it means rewriting serialised option values and user meta keys, and we have seen that break more sites than it has protected. The threat it mitigates (blind SQL injection that assumes wp_) is rare in 2026, so leave it alone on live sites.
Can I just enable auto-updates for everything?
Enable them for core minors and for plugins you have no customisations around. For anything load-bearing, especially WooCommerce extensions and page builders, auto-updating straight to production will eventually take down a checkout at 3am. The middle path we use: auto-update minors, and run majors through staging within 72 hours of release.
How do I secure a WordPress site I cannot stop clients editing?
Reduce their capabilities rather than trusting their judgement. Give editors the Editor role, not Administrator, keep plugin installation to two named admin accounts with 2FA, and put a WAF in front so unpatched plugin bugs have a holding pen. Then send a monthly inventory report so nothing gets installed quietly.
If you do nothing else this week, do two things: block PHP execution in your uploads directory, and open your plugin list and sort by last update date. The first takes five minutes and removes the most common path to site takeover. The second will tell you, honestly, how much risk you have been carrying without deciding to.
Then book the restore test. Everything else on this list is cheaper than finding out your backups were theoretical.


