WordPress Core: updates and hardening
Updates
- Always keep WordPress up to date — old versions no longer receive security patches.
- Enable automatic updates for minor core versions.
- Before major updates, make a backup and test on a staging environment.
- Download the core exclusively from wordpress.org. No nulled or modified builds.
- Regularly verify the integrity of core files with WP-CLI:
wp core verify-checksums. - Remove files from the site root that reveal the WP version and other details:
readme.html,license.txt,wp-config-sample.php. They are restored on core updates — automate their removal via a deploy script or cron.
wp-config.php configuration
Disabling file editing through the admin panel — protection against RCE; it disables the Theme Editor and Plugin Editor at once:
define('DISALLOW_FILE_EDIT', true);
Blocking plugin/theme installation and updates — only for production with CI/CD:
define('DISALLOW_FILE_MODS', true);
Forcing SSL for the admin panel:
define('FORCE_SSL_ADMIN', true);
Disabling the built-in wp-cron — replace it with a system cron:
define('DISABLE_WP_CRON', true);
*/15 * * * * wget -q -O - https://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1Disabling debug mode in production:
define('WP_DEBUG', false);
define('WP_DEBUG_DISPLAY', false);
define('WP_DEBUG_LOG', false);
Unique security keys (Salts)
- Generate them with the WordPress.org Salt Generator.
- Updating the keys “resets” all active sessions.
- Rotate the keys after any security incident.
Protecting wp-config.php
- Move the file one level above the web server’s root directory.
- Permissions:
600or640. - Block access at the web server level (see the “Server environment” section).
Themes and plugins
Choice and source
- Install only from the official WordPress.org catalog or trusted marketplaces.
- Before installing, assess: date of the last update, rating, number of active installations, reviews (especially regarding security).
- Avoid “pirated” (nulled) versions — they often contain backdoors.
Updates
- Regularly update all themes and plugins.
- Disable and remove unused modules — even a deactivated plugin can be vulnerable.
- Subscribe to notifications about critical updates from developers and vulnerability databases (Patchstack, WPScan).
Audit and verification
- Scan plugins/themes for vulnerabilities and malicious code: WPScan, Sucuri SiteCheck, Patchstack, Wordfence.
- For paid themes/plugins — check whether the developer has performed a security audit.
- Test new extensions first on a local or staging server.
- Check external dependencies (Composer, npm) for updates and CVEs.
Supply Chain Security
- Use Subresource Integrity (SRI) for external scripts and styles.
- Verify the checksums of downloaded packages.
- Keep an eye on supply chain attacks through CVE databases.
Server environment
PHP
- Use PHP 8.3 or 8.4 (the standard for 2026). Versions below 8.1 are a risk.
- Hide the PHP version:
expose_php = Off. - Configure
open_basedirto restrict PHP’s access to the file system.
Disable dangerous functions in php.ini:
disable_functions = exec, passthru, shell_exec, system, proc_open, popen, curl_multi_exec, parse_ini_file, show_source
File permissions
| Object | Permissions |
|---|---|
| WordPress files | 644 (or 640) |
| Directories | 755 (or 750) |
wp-config.php |
600 |
Web server configuration
Disabling directory listing:
- Apache:
Options -Indexesin.htaccess. - Nginx:
autoindex off;.
Blocking PHP execution in uploads:
Apache (.htaccess in wp-content/uploads/):
<FilesMatch "\.php$">
Require all denied
</FilesMatch>
Nginx:
location ~* /wp-content/uploads/.*\.php$ {
deny all;
}
Blocking PHP execution in wp-includes:
Nginx:
location ~* /wp-includes/.*\.php$ {
deny all;
}
Apache (.htaccess in wp-includes/):
<FilesMatch "\.php$">
Require all denied
</FilesMatch>
wp-includes/ms-files.php is needed for Multisite.Blocking access to system files:
Apache:
<FilesMatch "^(wp-config\.php|\.htaccess|\.git|\.env|\.svn|readme\.html|license\.txt)">
Require all denied
</FilesMatch>
Nginx:
location ~ /(\.|wp-config\.php|readme\.html|license\.txt) {
deny all;
}
Blocking xmlrpc.php at the server level:
Apache:
<Files xmlrpc.php>
Require all denied
</Files>
Nginx:
location = /xmlrpc.php {
deny all;
}
Security Headers
Configure them through the web server config or .htaccess:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
X-Permitted-Cross-Domain-Policies: none
Content-Security-Policy: [configure carefully for the specific project]
Blocking indexing of the admin panel
Add X-Robots-Tag: noindex, nofollow for /wp-admin/ and /wp-login.php so search engines do not index service pages:
Nginx:
location ~* ^/(wp-admin|wp-login\.php) {
add_header X-Robots-Tag "noindex, nofollow" always;
}
SSL/TLS
- Install an SSL certificate (Let’s Encrypt or a commercial one).
- Redirect all HTTP traffic to HTTPS.
- Enable HSTS.
SSH
- Use SSH with keys (not a password).
- Change the default SSH port.
- Use Fail2Ban to block suspicious connection attempts.
Limiting request size
Protection against DoS via large uploads. Set a limit at the web server level:
- Nginx:
client_max_body_size 10m; - Apache:
LimitRequestBody 10485760
upload_max_filesize and post_max_size in php.ini.Filtering HTTP methods
Allow only GET, POST, HEAD for public pages. Block TRACE, DELETE, PUT, OPTIONS (if they are not needed for the REST API):
Nginx:
if ($request_method !~ ^(GET|POST|HEAD)$) {
return 405;
}
Rate Limiting at the server level
Separately from plugins, set up rate limiting for wp-login.php and xmlrpc.php:
Nginx:
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
location = /wp-login.php {
limit_req zone=login burst=3 nodelay;
include fastcgi_params;
fastcgi_pass php;
}
Protection against hot-linking
Blocking direct embedding of images from other sites (bandwidth theft):
Nginx:
location ~* \.(jpg|jpeg|png|gif|webp|svg)$ {
valid_referers none blocked server_names *.yourdomain.com;
if ($invalid_referer) {
return 403;
}
}
Database
Configuration
- Change the default
wp_prefix to a random one (for example,x7z_) at the installation stage. - Restrict the DB user’s privileges: only
SELECT,INSERT,UPDATE,DELETE. NoDROP,ALTER,CREATE,GRANT(if that is possible in the running mode). - Restrict remote access to MySQL:
bind-address = 127.0.0.1in the configuration. - Regular rotation of the DB password.
- Use a separate DB user for each site on the server.
Authentication and access control
User registration
If the site does not need public registration — disable it: Settings → General → uncheck “Anyone can register”.
Passwords
- At least 12 characters: uppercase/lowercase letters, digits, special characters.
- Check passwords against the HaveIBeenPwned database.
- Use a password manager.
- Regular rotation of passwords.
Two-factor authentication (2FA)
- Mandatory for all administrators and editors.
- SMS is legacy. Use:
- Passkeys (biometrics, FIDO2) — the best option.
- TOTP (Google Authenticator, Authy) — a good option.
- Plugins: WP 2FA, Solid Security.
Protecting the login form
- Limiting login attempts: block the IP after 3–5 failed attempts for 10–20 minutes (Limit Login Attempts Reloaded or an equivalent).
- CAPTCHA (reCAPTCHA) on the login form and public forms.
- Changing the login URL from
/wp-adminto a custom one (security through obscurity, but an effective filter for bot noise). - Where possible — restrict access to the login page by IP through
.htaccessor a firewall.
Application Passwords
For REST API integrations (n8n, Zapier, etc.) use Application Passwords instead of the admin’s real password.
The principle of least privilege
- Grant the administrator role only to trusted users.
- Give everyone else the minimally necessary roles.
- Do not write content under the administrator account — create a separate “Author” or “Editor” account for publications. The admin account should not be the author of any post.
- Regularly review the list of users and delete unnecessary accounts.
- Do not leave the login
adminoradministrator.
Administrator User IDs
- Delete or downgrade the role of users with IDs 1–10 (bots and brute-force scripts attack these IDs first).
- Create a working admin account with an unpredictable ID (for example, 8+ or higher).
- Reassign the content of deleted users to the author account.
Sessions and cookies
- Set the
Secure,HttpOnly,SameSite=Strictflags for cookies. - Limit the lifetime of administrator sessions.
- Automatically end the sessions of inactive users (the Idle User Logout plugin).
- Automatic logout of all of a user’s sessions when their role or password is changed by another admin — this prevents a situation where a downgraded or dismissed user keeps an active session.
APIs and interfaces
XML-RPC
In 2026 it is practically unnecessary (replaced by the REST API), but it remains a vector for DDoS and brute-force attacks. Disable it through a filter:
add_filter('xmlrpc_enabled', '__return_false');
xmlrpc.php at the web server level (see the “Server environment” section).WordPress Embeds (oEmbed)
If the content embedding functionality (embed) is not needed — disable it to reduce the attack surface:
// Remove oEmbed discovery links
remove_action('wp_head', 'wp_oembed_add_discovery_links');
// Remove oEmbed JavaScript
remove_action('wp_head', 'wp_oembed_add_host_js');
// Disable the oEmbed endpoint
add_filter('embed_oembed_discover', '__return_false');
// Deactivate embed rewrite rules
remove_action('rest_api_init', 'wp_oembed_register_route');
Comments
If the project does not need commenting on the frontend — disable comments completely at the core level. Open comments are a vector for spam, XSS and SQL injections through third-party commenting plugins.
- In the admin panel: Settings → Discussion → uncheck “Allow people to submit comments on new posts”.
For a complete shutdown (including existing posts):
// Disable comment support for all content types
add_action('admin_init', function() {
$post_types = get_post_types();
foreach ($post_types as $post_type) {
remove_post_type_support($post_type, 'comments');
remove_post_type_support($post_type, 'trackbacks');
}
});
// Close comments on the frontend
add_filter('comments_open', '__return_false', 20, 2);
add_filter('pings_open', '__return_false', 20, 2);
// Hide existing comments
add_filter('comments_array', '__return_empty_array', 10, 2);
// Remove the "Comments" menu item from the admin panel
add_action('admin_menu', function() {
remove_menu_page('edit-comments.php');
});
Pingbacks and Trackbacks
Disable them completely — this is legacy functionality used as a vector for DDoS amplification and spam.
- In the admin panel: Settings → Discussion → uncheck “Allow link notifications from other blogs (pingbacks and trackbacks) on new posts”.
Additionally through code:
add_filter('xmlrpc_methods', function($methods) {
unset($methods['pingback.ping']);
unset($methods['pingback.extensions.getPingbacks']);
return $methods;
});
add_filter('wp_headers', function($headers) {
unset($headers['X-Pingback']);
return $headers;
});
REST API
Restricting the REST API for anonymous users:
- An ordinary corporate site
- A landing page
- A blog without external integrations and headless architecture
- Headless WP (React/Vue/Next.js)
- WooCommerce with external integrations
- Form or caching plugins that work through REST
- Gutenberg blocks on the frontend that pull data through the API
The optimal approach — do not disable it completely, but selectively close dangerous endpoints (at least /wp/v2/users) and allow only the needed namespaces (for example, contact-form-7, oembed).
Restrict access to the endpoints that list users (/wp-json/wp/v2/users):
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;
});
If the REST API is not used publicly — restrict access to authenticated users only.
User Enumeration
Block /?author=N enumeration:
if (!is_admin() && isset($_GET['author'])) {
wp_redirect(home_url(), 301);
exit;
}
Or through .htaccess / Nginx rewrite.
Hiding IDs (Posts, Users, Terms)
Hide the numeric IDs of users, posts and terms from public URLs and HTML markup — they make enumeration and targeted attacks easier.
- Users: use a slug instead of an ID in author archives; remove the ID from the body class (
author-1) through thebody_classfilter. - Posts: where possible, use pretty permalinks without numeric IDs.
Remove IDs from CSS classes (post-123, page-item-45):
add_filter('post_class', function($classes) {
return array_filter($classes, function($class) {
return !preg_match('/^(post|page-item)-\d+$/', $class);
});
});
Terms: similarly, remove IDs from menu and widget classes (menu-item-123, cat-item-5):
add_filter('nav_menu_css_class', function($classes) {
return array_filter($classes, function($class) {
return !preg_match('/^menu-item-\d+$/', $class);
});
});
Remove <meta name="generator"> from the head:
remove_action('wp_head', 'wp_generator');
File Uploads
- Validate the MIME types of uploaded files (do not rely on the extension alone).
- Block PHP execution in
wp-content/uploads/(see the “Server environment” section). - Scan uploaded files for malicious code.
Strictly limit the allowed file types through the upload_mimes filter — leave only the formats the project actually needs. By default, WordPress allows dozens of types (including SVG in newer versions), most of which are unnecessary:
add_filter('upload_mimes', function($mimes) {
return [
'jpg|jpeg|jpe' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
'pdf' => 'application/pdf',
];
});
.svg (an XSS vector without sanitization), .exe, .php, .sh, .sql.Content-Disposition header — configure the server to serve files from uploads with the Content-Disposition: attachment header, so the browser does not execute them but offers them for download:
Nginx:
location ~* /wp-content/uploads/ {
add_header Content-Disposition "attachment";
}
WAF and network protection
External WAF (CDN)
- Cloudflare (or Sucuri, Cloud Armor) — recommended for all projects.
- A CDN hides the server’s real IP address and protects against DDoS.
Configure WAF Rules:
- Block access to
xmlrpc.php. - Challenge (Captcha) for
/wp-adminfor all countries except yours. - Block requests with suspicious User-Agents.
- Rate limiting on critical endpoints.
Internal WAF (plugins)
- Wordfence, Solid Security or equivalents — for protection at the WordPress level.
- Monitoring and blocking of suspicious requests.
Network firewall
- Configure iptables/ufw: close all ports except the necessary ones (SSH, HTTPS).
- Fail2Ban or OSSEC for automatic blocking of suspicious IPs.
Monitoring and logging
Activity Log
- Install WP Activity Log or an equivalent.
- Record: logins, plugin installations/updates, settings changes, content edits.
Login Notification
Set up an email notification to the admin on every login to the admin panel — this lets you promptly detect unauthorized access, even if the attacker has bypassed the other layers of protection.
File Integrity Monitoring
- Track changes in the hashes of core, plugin and theme files (Wordfence, OSSEC).
wp core verify-checksums— regularly through cron.
Server logs
- Regularly analyze access and error logs for suspicious activity.
- Set up alerts for an anomalous number of 404s, 403s or attempts to access the admin panel.
Backups
- At least 3 copies in different storage locations (server + cloud: S3, Google Drive, Backblaze).
- Never store backups only on the same server.
- Frequency: daily for active sites, weekly for static ones.
- Incremental + full backups.
- Automate them through plugins (UpdraftPlus, BlogVault) or server scripts.
- Store backups encrypted.
DNS and Email Security
DNS
- Enable DNSSEC to protect against DNS spoofing.
- Hide the server’s real IP behind a CDN (Cloudflare, etc.).
Email (protecting the domain from phishing)
| Record | Purpose |
|---|---|
| SPF | Specify the servers allowed to send mail |
| DKIM | Digital signature for emails |
| DMARC | Policy for handling emails that fail verification |
Disclosure and documentation
security.txt
Add a /.well-known/security.txt file (RFC 9116) for responsible disclosure:
Contact: mailto:security@example.com
Preferred-Languages: uk, en
Expires: 2027-01-01T00:00:00.000Z
Documentation
- Document all security settings (without passwords in plain text).
- Develop a disaster recovery plan (DRP).
- Keep the documentation up to date.
Administrator workstation security
- An up-to-date OS, browser, antivirus/EDR.
- Do not use the admin account for ordinary browsing.
- Avoid public Wi-Fi when working with the site; use a VPN.
- Train the team: phishing, social engineering, the mandatory nature of 2FA.
- Assign someone responsible for security monitoring.
Action Plan (quick start)
The minimal set of actions for basic protection of a new WordPress project:
- Check the PHP version (8.3+)
- Add
DISALLOW_FILE_EDITandDISABLE_WP_CRONtowp-config.php - Update the Salts
- Set up 2FA for all admins
- Block
xmlrpc.phpat the server level - Disable pingbacks/trackbacks
- Disable comments (if the project does not need them)
- Limit the allowed file upload formats (
upload_mimes) - Block PHP execution in
uploads/andwp-includes/ - Remove
readme.html,license.txt,wp-config-sample.php - Block user enumeration (author, REST API users)
- Hide the IDs of users, posts and terms from public markup
- Delete/downgrade admin accounts with IDs 1–10, create an admin with a higher ID
- Configure Security Headers
- Set up backups to external storage
- Install an Activity Log plugin
- Check file permissions (644/755/600)
- Configure Cloudflare WAF
- Set up rate limiting on
wp-login.phpat the server level - Filter HTTP methods (only GET/POST/HEAD)
- Set up login notifications for admins
- Set up protection against hot-linking
- Configure SPF/DKIM/DMARC
- Add
security.txt - Disable public registration (if not needed)
- Disable oEmbed (if not needed)
- Add an X-Robots-Tag noindex for wp-admin
- Test restoration from a backup
- Encrypt DB dumps separately