1. Add following rule to your WAF
If using Cloudflare goto Security Rules -> New Custom Rule and add this rule:
(http.request.uri.path contains "xmlrpc.php") or (http.request.uri.path contains ".env") or (http.request.uri.path contains ".git") or (http.request.uri.path contains "/.well-known/sg-hosted-ping") or (http.request.uri.path contains "wp-config.php") or (http.request.uri.path contains "wp-config.bak") or (http.request.uri.path contains "/etc/passwd") or (http.request.uri.path contains "eval(") or (http.request.uri.path contains "base64_decode") or (http.user_agent contains "CensysInspect") or (http.user_agent contains "Shodan") or (http.user_agent contains "Masscan") or (http.user_agent contains "ZGrab")
set action to Block
2. Block XML-RPC
In .htaccess file in your WordPress directory:
# Block XML-RPC
<Files xmlrpc.php>
Order Deny,Allow
Deny from all
</Files>
XML-RPC is a legacy WordPress feature that:
- Is used in brute force attacks (one request can test thousands of username/password combinations)
- Is used in DDoS amplification attacks (pingback feature)
- Is not needed unless you use the WordPress mobile app or certain integrations
Verify it’s blocked:
curl -I https://your-website.com/xmlrpc.php
# Expected: 403 Forbidden
3. Block malicious URI patterns
In .htaccess file in your WordPress directory:
<IfModule mod_rewrite.c>
RewriteEngine On
# Block malicious URI patterns
# a RewriteCond directive acts like an if condition, and the immediately following RewriteRule is the action executed if that condition evaluates to true.
# [F] = return Forbidden 403
# [L] = sop processing rules
RewriteCond %{REQUEST_URI} (xmlrpc\.php|\.env|\.git|sg-hosted-ping|wp-config\.php|wp-config\.bak|/etc/passwd|eval\(|base64_decode) [NC]
RewriteRule .* - [F,L]
</IfModule>
4. Block direct execution of .php files under includes and uploads folders
# Block direct execution of PHP files in wp-includes and uploads
RewriteRule ^wp-includes/.*\.php$ - [F,L]
RewriteRule ^wp-content/uploads/.*\.php$ - [F,L]
RewriteRule ^wp-admin/includes/.*\.php$ - [F,L]
5. Block direct execution of any .php file inside wp-content/ and its subdirectories
Inside wp-content create a .htaccess file with following:
<FilesMatch "\.(?i:php)$">
<IfModule !mod_authz_core.c>
Order allow,deny
Deny from all
</IfModule>
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
</FilesMatch>
6. Don’t give apache / NGINX write permission on WordPress directory
Only give www-data write permission to wp-content/uploads
7. Don’t use default database, table and user names
Change table prefix from wp_ to something else
8. Block FTP-based attacks and plugin crashes
// Filesystem — prevents FTP-based attacks and plugin crashes
// Tells WordPress to write files directly. Without this, WordPress uses FTP for file writes
define('FS_METHOD', 'direct');
// Force HTTPS for admin
define('FORCE_SSL_ADMIN', true);
// Disable wp-cron (replaced with system cron — no public endpoint)
define('DISABLE_WP_CRON', true);
// Auto-update core for security patches only. **It doesn't work though**
define('WP_AUTO_UPDATE_CORE', 'minor');
// Cloudflare proxy detection
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}
9. If WordPress and MySQL are running on same machine then MySQL only needs to listen on localhost
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
Find and set:
bind-address = 127.0.0.1
sudo systemctl restart mysql
verify:
$ sudo ss -tlnp | grep mysql
LISTEN 0 151 127.0.0.1:3306 0.0.0.0:* users:(("mysqld",pid=1239286,fd=29))
LISTEN 0 70 127.0.0.1:33060 0.0.0.0:* users:(("mysqld",pid=1239286,fd=21))
10. Change the login URL from /wp-login.php to something else
The Security Optimizer plugin (sg-security) by SiteGround has this feature allowing you to change the default login URL. It also allows you to add 2FA. There are other plugins also that support this.