WordPress powers a huge share of the web, which makes it a constant target for automated attacks. The good news is that WordPress core is mature, actively maintained and patched quickly. The overwhelming majority of real-world compromises come through the same few doors: outdated or abandoned plugins, weak or reused passwords, poorly configured hosting and missing backups. Security is not a single plugin you install; it is a set of layers, each of which limits the damage if another fails. This checklist walks through those layers in order of impact, with the configuration snippets we actually use.
Understand the threat model first#
Most attacks on WordPress sites are not personal. Bots scan millions of sites for known vulnerable plugin versions, try leaked username and password combinations against wp-login.php and XML-RPC, and look for exposed files such as backups or debug logs. A successful compromise typically ends in SEO spam, redirects to scam pages, malware distribution, card skimming on checkout pages or the server being used to attack others.
Industry vulnerability reports from WordPress security vendors consistently show that the vast majority of newly disclosed WordPress vulnerabilities are in plugins, with a smaller share in themes and only a handful in core. The most common classes match the broader OWASP Top 10: cross-site scripting, broken access control, injection and cross-site request forgery. That tells you where to focus.
| Layer | Main risk addressed | Key controls | Effort |
|---|---|---|---|
| Updates & plugin hygiene | Known vulnerabilities | Timely updates, fewer plugins, no abandoned or nulled code | Low, ongoing |
| Authentication | Credential stuffing, brute force | Unique passwords, 2FA, least privilege, login rate limiting | Low |
| Configuration | Escalation after a breach | wp-config hardening, disabled file editing, debug off | Low |
| Server | Code execution, data exposure | Supported PHP, file permissions, no PHP in uploads, TLS | Medium |
| Browser | XSS impact, clickjacking | Security headers, Content Security Policy | Medium |
| Detection & recovery | Undetected compromise, data loss | WAF, monitoring, logging, tested off-site backups | Medium, ongoing |
Layer 1: Updates and plugin hygiene#
Keeping software current is the highest-impact security task, and the least glamorous. WordPress has applied minor (security and maintenance) core releases automatically since version 3.7, and since version 5.5 you can enable automatic updates for individual plugins and themes from the dashboard.
Update strategy#
- Core minor releases: leave automatic updates on. They are small and focused on fixes.
- Plugins and themes: enable auto-updates for simple, well-maintained plugins. For business-critical extensions — payment gateways, membership systems, page builders — test updates on a staging copy first, but do it within days, not months.
- Security releases: apply immediately. Attackers often begin scanning for a vulnerability within hours of its public disclosure.
Reduce your attack surface#
Every plugin is code running with full access to your database. Remove anything deactivated (inactive plugins can still be exploited if their files are reachable), replace plugins that have not been updated in a long time, and prefer one well-maintained plugin over three small ones doing overlapping jobs. Never install “nulled” (pirated) premium themes or plugins: they are a common delivery mechanism for backdoors.
Your PHP version is part of this layer too. Branches that are past end-of-life receive no security fixes; our overview of modern PHP 8 includes the current support timeline.
Layer 2: Authentication and access control#
Credential attacks are relentless and cheap for attackers. Make them useless.
- Unique, long passwords for every account, stored in a password manager. Reused passwords from other breaches are the fuel for credential stuffing.
- Two-factor authentication for every account that can publish or administer — at minimum administrators, editors and shop managers. The Two Factor plugin maintained by WordPress contributors, or the 2FA feature of a reputable security suite, both work; passkeys are increasingly supported too.
- Least privilege. Most people who write content need the Editor or Author role, not Administrator. Audit users regularly and remove former staff and agencies promptly.
- Rate-limit logins. Limit failed attempts at the server, CDN or plugin level to blunt brute-force attacks.
- Application passwords, available since WordPress 5.6, let integrations use the REST API with revocable, per-application credentials instead of a real user password.
XML-RPC and user enumeration#
XML-RPC (xmlrpc.php) predates the REST API and supports methods that let attackers try many passwords in a single request. If nothing on your site needs it, block it at the server or with a plugin. Similarly, author archives and the REST users endpoint reveal usernames; that is not a vulnerability in itself, but there is little reason to make usernames easy to collect, and it makes strong passwords and 2FA even more important.
Layer 3: Hardening wp-config.php#
A few constants in wp-config.php remove capabilities an attacker would love to have after compromising an admin account. The official Hardening WordPress handbook covers them in detail.
<?php
// Disable the built-in theme and plugin file editors
define( 'DISALLOW_FILE_EDIT', true );
// Optional, stricter: also block installing/updating plugins and themes from the dashboard.
// Only use this if updates are deployed another way (Git, WP-CLI, managed host).
// define( 'DISALLOW_FILE_MODS', true );
// Force HTTPS for logins and the admin area
define( 'FORCE_SSL_ADMIN', true );
// Never display errors on production; log them privately instead
define( 'WP_DEBUG', false );
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', '0' );
// Unique authentication keys and salts (generate fresh values, never reuse examples)
define( 'AUTH_KEY', 'paste-a-long-random-unique-value-here' );
// … SECURE_AUTH_KEY, LOGGED_IN_KEY, NONCE_KEY and the four *_SALT constants
A few further points:
- Regenerate keys and salts after any suspected compromise; this logs out every user. WP-CLI makes it one command:
wp config shuffle-salts. - Move
wp-config.phpone directory above the web root where your hosting allows it. WordPress looks for it there automatically. - Use a dedicated database user with privileges only on the site’s database, and a strong password.
- Changing the table prefix from
wp_is obscurity, not security. It is harmless on new installs but not worth risking a migration on an existing site.
Layer 4: Server and file system#
File permissions and ownership#
As a baseline, directories should be 755 and files 644, owned by your account user rather than the web server user where the hosting setup allows. Tighten wp-config.php to 440 or 400. Never use 777; if a plugin “needs” it, the server configuration is wrong.
Block PHP execution in uploads#
The uploads directory must be writable, which makes it the favourite place to drop a web shell. It should never execute PHP. On Apache, add this to wp-content/uploads/.htaccess:
<FilesMatch "\.(php|phtml|phar|php\d)$">
Require all denied
</FilesMatch>
On Nginx, inside the server block:
# No PHP execution in uploads
location ~* ^/wp-content/uploads/.*\.(php|phtml|phar)$ {
deny all;
}
# Hide sensitive files
location ~* /(wp-config\.php|readme\.html|license\.txt|\.env|\.git) {
deny all;
}
# Block XML-RPC if nothing uses it
location = /xmlrpc.php {
deny all;
}
Everything else on the server#
- Serve everything over HTTPS and redirect HTTP to HTTPS.
- Disable directory listing and make sure backups, SQL dumps and
debug.logare never stored in a public directory. - Use SFTP or SSH keys, never plain FTP.
- Isolate sites from each other: one compromised site on a shared account can infect every other site in the same account.
How much of this you control depends on your hosting. Managed WordPress hosts handle much of it for you; on a VPS, it is your job. We compare the trade-offs in Choosing Web Hosting.
Layer 5: HTTP security headers#
Security headers tell browsers to enforce protections on your behalf. They limit the impact of cross-site scripting, prevent clickjacking and control what information leaks to other sites.
| Header | Purpose | Typical starting value |
|---|---|---|
Strict-Transport-Security |
Forces HTTPS on future visits | max-age=31536000; includeSubDomains |
Content-Security-Policy |
Restricts where scripts and other resources can load from | Start with frame-ancestors 'self', then build up in report-only mode |
X-Content-Type-Options |
Stops MIME-type sniffing | nosniff |
Referrer-Policy |
Limits URL data sent to other sites | strict-origin-when-cross-origin |
Permissions-Policy |
Disables browser features you don’t use | camera=(), microphone=(), geolocation=() |
X-Frame-Options |
Legacy clickjacking protection | SAMEORIGIN |
A full Content Security Policy is the most powerful header and the hardest to deploy on WordPress, because themes and plugins often inject inline scripts. Roll it out with Content-Security-Policy-Report-Only first, collect violations, then enforce. On WooCommerce checkout pages, where payment card skimming scripts are a real threat, a tight policy is especially valuable.
Layer 6: Writing secure custom code#
If you or your agency write custom themes or plugins, most vulnerabilities come down to four habits. WordPress provides APIs for each, documented in the Security section of the Common APIs Handbook:
- Check capabilities with
current_user_can()before doing anything privileged — being logged in is not authorization. - Verify intent with nonces to prevent cross-site request forgery.
- Validate and sanitize input on the way in.
- Escape output late, at the moment of printing, with the function that matches the context.
<?php
add_action( 'admin_post_save_store_notice', function () {
// 1. Authorization
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You are not allowed to do this.', 'hiddlins' ), 403 );
}
// 2. Intent (CSRF protection)
check_admin_referer( 'save_store_notice' );
// 3. Validate and sanitize
$notice = sanitize_text_field( wp_unslash( $_POST['notice'] ?? '' ) );
$limit = absint( $_POST['limit'] ?? 0 );
update_option( 'store_notice', $notice );
// Database queries: always use prepared statements
global $wpdb;
$orders = $wpdb->get_results(
$wpdb->prepare(
"SELECT id, total FROM {$wpdb->prefix}custom_orders WHERE status = %s LIMIT %d",
'pending',
$limit
)
);
wp_safe_redirect( admin_url( 'options-general.php?page=store-notice&updated=1' ) );
exit;
} );
// 4. Escape late, by context
printf(
'<a href="%s" title="%s">%s</a>',
esc_url( $link ),
esc_attr( $title ),
esc_html( $label )
);
Layer 7: Firewalls, monitoring and backups#
Web application firewall#
A WAF filters malicious requests before they reach WordPress. Edge firewalls run at a CDN or proxy and stop traffic before it consumes your server’s resources; plugin-based firewalls run inside WordPress and understand its context better but share the server’s load. Either is a useful extra layer — and virtual patching can block a known exploit before you have applied the update.
Monitoring and integrity checks#
- Uptime and content monitoring to notice defacements or spam injections quickly.
- File integrity checks:
wp core verify-checksumsandwp plugin verify-checksums --allcompare your files against the official versions on WordPress.org. - Activity logging of logins, role changes, plugin installs and settings changes, so you can reconstruct what happened.
- Search Console security issue alerts, which often flag injected spam before visitors notice.
Backups you have actually tested#
Backups are your last line of defence and the one that makes every other failure recoverable. Follow the 3-2-1 principle: at least three copies, on two different types of storage, with one off-site and outside the reach of your hosting account’s credentials. Include both files and the database, keep enough history to restore from before an undetected infection, and — most importantly — test a restore regularly. A backup you have never restored is a hope, not a plan.
- Take a snapshot of the compromised site for analysis before changing anything, then put the site into maintenance mode.
- Change every password: WordPress users, hosting, SFTP/SSH, database, and any API keys stored on the site. Shuffle the salts.
- Find the entry point (outdated plugin, stolen credentials, another site on the same account) — otherwise reinfection is likely.
- Restore from a known-clean backup or replace core, plugins and themes with fresh copies, then review uploads and the database for injected content and rogue admin users.
- Update everything, monitor closely, and request a review in Search Console if Google flagged the site.
The condensed checklist#
- Update core, plugins, themes and PHP promptly; remove anything unused, abandoned or nulled.
- Unique passwords, two-factor authentication and least-privilege roles for every user.
- Disable file editing, force HTTPS for admin, keep debug output off and regenerate salts after incidents.
- Correct file permissions, no PHP execution in uploads, sensitive files blocked, XML-RPC off if unused.
- Add security headers and work towards a Content Security Policy, especially on checkout.
- In custom code: check capabilities, verify nonces, sanitize input, escape output, prepare queries.
- Run a WAF, monitor file integrity and activity, and keep tested, off-site backups.
Security also intersects with SEO: a hacked site can be flagged in search results or deindexed, which is why we include HTTPS, server headers and clean indexation in our technical SEO checklist. Online stores carry extra responsibility because they handle customer data; the same plugin discipline that keeps them secure also keeps them fast, as covered in our WooCommerce performance guide.
Conclusion: make security a routine#
None of the layers in this checklist is difficult on its own. What makes WordPress sites vulnerable is that security tasks are easy to postpone: an update skipped here, a former contractor’s admin account left active there, a backup nobody has tested. Layered defences, applied consistently, turn most attacks into non-events — and make the rare successful one recoverable.
If you would rather not carry that routine yourself, our Website Care & Maintenance Plan handles updates, monitoring, off-site backups and security hardening every month. Building something new? Our WordPress Website Development service ships with these defences in place from day one. Contact us and tell us about your site.
Frequently asked questions
Is WordPress secure enough for a business website?
Yes, when it is maintained properly. WordPress core has a dedicated security team and automatic security releases. Most compromises come from outdated or abandoned plugins and themes, weak or reused passwords and poor hosting configuration, all of which you can control.
Do I need a security plugin?
A security plugin can help with login protection, firewall rules and file change monitoring, but it is not a substitute for updates, strong authentication, sensible server configuration and backups. Choose one well-maintained tool rather than stacking several that overlap.
Should I disable XML-RPC?
If nothing on your site uses it, yes. XML-RPC is a common target for brute-force login attempts. Some tools, such as certain mobile apps and older integrations, still rely on it, so check first; most modern integrations use the REST API with application passwords instead.
How often should I back up my WordPress site?
Match backup frequency to how often the site changes. A brochure site may be fine with daily backups, while a busy WooCommerce store needs frequent database backups. Always keep copies off the server and test restores regularly.
What should I do first if my WordPress site is hacked?
Take a fresh backup of the compromised state for analysis, put the site into maintenance mode, and change all passwords and keys. Then identify the entry point, clean or restore from a known-good backup, update everything, and monitor closely afterwards.
