In the world of cybersecurity, there is a concept known as “Reconnaissance”.
Before a hacker (or an automated bot) attacks your site, they scan it. They look for low-hanging fruit. If your website proudly announces: “Hello! I am running WordPress 5.8.1!”, you are practically rolling out the red carpet for them. Because they know exactly which vulnerabilities existed in 5.8.1.
In this comprehensive security guide, we will go beyond simple “Hide Version” plugins. We will dive into Security by Obscurity (and why it is only the first layer of defense), and then we will implement real, robust server-level hardening using HTTP Headers, login protection, and automated monitoring.
Part 1: Understanding “Security Through Obscurity”
What is Security Through Obscurity?
Security through obscurity is the practice of hiding information about your system to make it harder for attackers to find vulnerabilities. While controversial in security circles, when combined with proper security measures, it adds an extra layer of defense.
The Debate:
- Critics say: “A system should be secure even if attackers know everything about it”
- Practitioners say: “Why make it easier for attackers by broadcasting your vulnerabilities?”
The Truth: Security through obscurity is not sufficient on its own, but it is a valuable first layer of defense. Think of it as removing the welcome mat from your front door—it won’t stop a determined burglar, but it might make opportunistic ones move on to an easier target.
Why Hiding WordPress Version Matters
WordPress core vulnerabilities are publicly documented. When a security patch is released, the changelog explicitly states which versions are affected. By displaying your version number, you’re essentially telling attackers:
- Which known vulnerabilities might exist on your site
- Whether you’ve applied the latest security patches
- What specific exploit code might work against you
Common Version Leakage Points:
- HTML meta generator tag
- RSS/Atom feed generator tags
- Script and style query strings (
?ver=6.7.1) - Readme.html file in root directory
- WordPress REST API responses
Part 2: Removing WordPress Version Information
The Generator Meta Tag
By default, WordPress injects a meta tag into the <head> of your HTML:
<meta name="generator" content="WordPress 6.7.1" />
This is useless for SEO and dangerous for security.
The Fix (PHP snippet)
Do not install a heavyweight plugin just for this. Add this cleaned-up code to your functions.php:
/**
* Remove WordPress Version from Head and Feeds
*/
remove_action('wp_head', 'wp_generator');
remove_action('rss2_head', 'the_generator');
remove_action('commentsrss2_head', 'the_generator');
remove_action('wp_head', 'wlwmanifest_link'); // Windows Live Writer (Legacy)
remove_action('wp_head', 'rsd_link'); // Really Simple Discovery
/**
* Remove Version from Scripts and Styles
*/
function wppoland_remove_version_scripts_styles($src) {
if (strpos($src, 'ver=')) {
$src = remove_query_arg('ver', $src);
}
return $src;
}
add_filter('style_loader_src', 'wppoland_remove_version_scripts_styles', 9999);
add_filter('script_loader_src', 'wppoland_remove_version_scripts_styles', 9999);
/**
* Remove Version from RSS Feeds
*/
add_filter('the_generator', '__return_empty_string');
Why this matters: It breaks the logic of simple “Script Kiddie” scrapers. If a bot is scanning 1 million sites looking for “WordPress 5.X”, your site will return a blank stare. The bot moves on.
Important Note: This is Security by Obscurity. It’s the first layer, not the only layer. A determined attacker can still fingerprint WordPress through other methods (REST API endpoints, file structure, etc.).
Additional Version Removal Methods
Remove from REST API
/**
* Remove WordPress version from REST API headers
*/
add_filter('rest_api_init', function() {
remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
}, 15);
add_filter('rest_pre_serve_request', function($value) {
header('X-Powered-By: WordPress');
return $value;
});
Remove Readme and License Files
/**
* Block access to readme.html and license.txt
*/
function wppoland_block_readme_access() {
if (strpos($_SERVER['REQUEST_URI'], 'readme.html') !== false ||
strpos($_SERVER['REQUEST_URI'], 'license.txt') !== false) {
wp_die('Access denied', '403 Forbidden', array('response' => 403));
}
}
add_action('init', 'wppoland_block_readme_access');
Or via .htaccess:
<FilesMatch "^(readme\.html|license\.txt)$">
Order deny,allow
Deny from all
</FilesMatch>
Part 3: HTTP Security Headers (The Real Defense)
Hiding the version is like taking the house number off your door. It helps, but it doesn’t lock the door. HTTP Security Headers are the deadbolts.
In 2026, browsers are strict. If you don’t send the right headers, you are vulnerable to XSS (Cross-Site Scripting), Clickjacking, and data leaks.
1. X-Frame-Options (Anti-Clickjacking)
This header tells the browser: “Do not allow anyone to put my site inside an <iframe>.”
This prevents hackers from creating a fake site that loads your bank/login page in an invisible frame and capturing your clicks.
Implementation (.htaccess):
<IfModule mod_headers.c>
Header always append X-Frame-Options SAMEORIGIN
</IfModule>
Nginx:
add_header X-Frame-Options "SAMEORIGIN" always;
Options:
DENY- Page cannot be displayed in a frameSAMEORIGIN- Page can only be displayed in a frame on the same originALLOW-FROM uri- Page can only be displayed in a frame on the specified origin (deprecated)
2. X-Content-Type-Options
Prevents “MIME Sniffing”. If a hacker uploads a .jpg file that actually contains executable JavaScript, this header tells the browser: “The server said it’s an image, so treat it as an image, don’t execute it.”
Implementation:
<IfModule mod_headers.c>
Header set X-Content-Type-Options nosniff
</IfModule>
Nginx:
add_header X-Content-Type-Options "nosniff" always;
3. Strict-Transport-Security (HSTS)
This is critical for SSL. It forces the browser to always use HTTPS, even if the user types http://.
It prevents “Man-in-the-Middle” attacks on public Wi-Fi.
Implementation (Only do this if you have valid SSL!):
<IfModule mod_headers.c>
Header set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
</IfModule>
Nginx:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
What the parameters mean:
max-age=31536000: Browser remembers for 1 yearincludeSubDomains: Apply to all subdomains (e.g., blog.yoursite.com)preload: Submit to browser preload lists (optional but recommended)
Warning: Once you set this header, browsers will refuse to connect via HTTP for the specified duration. Make sure HTTPS is working perfectly before implementing HSTS.
4. Content-Security-Policy (CSP)
This is the most powerful header. It tells the browser: “Only execute scripts from these trusted sources.”
Basic Implementation:
<IfModule mod_headers.c>
Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;"
</IfModule>
Breaking it down:
default-src 'self': By default, only load resources from your own domainscript-src 'self' 'unsafe-inline': Allow scripts from your domain and inline scripts (needed for WordPress)img-src 'self' data: https:: Allow images from your domain, data URIs, and any HTTPS sourcestyle-src 'self' 'unsafe-inline': Allow styles from your domain and inline styles
Warning: CSP can break your site if misconfigured. Test in “report-only” mode first:
Header set Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report.php"
5. Referrer-Policy
Controls how much information is sent when a user clicks a link to another site.
Implementation:
<IfModule mod_headers.c>
Header set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
Nginx:
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
This sends the full URL to same-origin requests, but only the origin (domain) to cross-origin requests.
Options:
no-referrer- Never send referrer informationno-referrer-when-downgrade- Don’t send referrer when going from HTTPS to HTTPorigin- Only send the originstrict-origin- Only send origin, never when downgradingsame-origin- Full URL for same origin, no referrer for cross-originstrict-origin-when-cross-origin- Full URL for same origin, origin only for cross-origin
6. Permissions-Policy (formerly Feature-Policy)
Disables browser features you don’t use (camera, microphone, geolocation).
Implementation:
<IfModule mod_headers.c>
Header set Permissions-Policy "camera=(), microphone=(), geolocation=()"
</IfModule>
Nginx:
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
Common directives:
camera=()- Disable camera accessmicrophone=()- Disable microphone accessgeolocation=()- Disable geolocationpayment=()- Disable payment APIusb=()- Disable USB access
7. X-XSS-Protection
While largely superseded by CSP, this header provides additional XSS protection for older browsers.
<IfModule mod_headers.c>
Header set X-XSS-Protection "1; mode=block"
</IfModule>
Part 4: Disable XML-RPC
xmlrpc.php is a legacy API file. In the modern era of REST API, it is obsolete.
However, it is the #1 target for Brute Force attacks because it allows a hacker to try 500 passwords in a single HTTP request (the “multicall” method).
How to kill it:
Method 1: .htaccess
<Files xmlrpc.php>
order deny,allow
deny from all
</Files>
Method 2: functions.php
add_filter('xmlrpc_enabled', '__return_false');
Method 3: Nginx
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
}
Exception: If you use Jetpack or WordPress mobile app, you need XML-RPC. In that case, use a plugin like “Disable XML-RPC Pingback” to block only the dangerous methods.
Part 5: Login Hardening
The /wp-admin/ and /wp-login.php endpoints are under constant attack. Here’s how to protect them.
1. Limit Login Attempts
By default, WordPress allows unlimited login attempts. A bot can try 10,000 passwords.
Solution: Install “Limit Login Attempts Reloaded” Or add this code:
function wppoland_limit_login_attempts() {
$ip = $_SERVER['REMOTE_ADDR'];
$attempts = get_transient('login_attempts_' . $ip);
if ($attempts >= 5) {
wp_die('Too many login attempts. Try again in 15 minutes.');
}
}
add_action('wp_login_failed', function() {
$ip = $_SERVER['REMOTE_ADDR'];
$attempts = get_transient('login_attempts_' . $ip) ?: 0;
set_transient('login_attempts_' . $ip, $attempts + 1, 15 * MINUTE_IN_SECONDS);
});
2. Two-Factor Authentication (2FA)
Even if a hacker gets your password, they can’t log in without the 2FA code.
Recommended Plugins:
- WP 2FA (Official WordPress.org plugin)
- Two Factor Authentication (by miniOrange)
- Duo Two-Factor Authentication
3. Change the Login URL
By default, everyone knows your login is at /wp-login.php. Change it.
Plugin: WPS Hide Login
This changes the URL to something like /my-secret-login/. Bots scanning for /wp-login.php get a 404.
4. Disable User Enumeration
By default, you can discover usernames by visiting /?author=1.
Fix:
// Disable user enumeration
add_action('template_redirect', function() {
if (is_author()) {
wp_redirect(home_url(), 301);
exit;
}
});
// Block REST API user endpoint
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;
});
Part 6: Database Security
1. Change Table Prefix
By default, WordPress uses wp_ as the table prefix. This makes SQL injection easier.
During Installation: Set a custom prefix like xyz_ or prod_.
After Installation: Use a plugin like “Change Table Prefix” or manually update via PHPMyAdmin (advanced).
2. Disable File Editing
WordPress allows admins to edit theme/plugin files from the dashboard. If a hacker gets admin access, they can inject malware.
Disable it in wp-config.php:
define('DISALLOW_FILE_EDIT', true);
3. Secure wp-config.php
This file contains your database credentials. Protect it.
Move it one directory up: WordPress will still find it, but it’s outside the web root.
Or set strict permissions:
chmod 400 wp-config.php
Part 7: File Permissions
Often overlooked. If your file permissions are wrong, a hacker who compromises one plugin can rewrite your entire core.
The Golden Rules:
- Directories:
755(Read/Execute for everyone, Write only for owner) - Files:
644(Read for everyone, Write only for owner) - wp-config.php:
400or440(Read ONLY for owner/server)
Set them all at once:
find /path/to/wordpress -type d -exec chmod 755 {} \;
find /path/to/wordpress -type f -exec chmod 644 {} \;
chmod 400 wp-config.php
Part 8: Security monitoring
We do not recommend installing security plugins. Prefer server-level monitoring (log analysis, fail2ban, hosting WAF), regular backups, and hardening (strong passwords, updates, limited login attempts). Security is built through configuration and server measures, not plugins.
Part 9: Complete Security Checklist
Immediate Actions (Do Today)
- Remove WordPress version from HTML/feeds
- Remove version strings from CSS/JS
- Set X-Frame-Options header
- Set X-Content-Type-Options header
- Set Strict-Transport-Security (HSTS) if using SSL
- Disable XML-RPC (if not needed)
- Install Limit Login Attempts
- Change default “admin” username
Short-term Actions (This Week)
- Implement Content-Security-Policy
- Set Referrer-Policy
- Enable Two-Factor Authentication
- Change login URL
- Disable user enumeration
- Set correct file permissions (755/644)
Long-term Actions (This Month)
- Change database table prefix (if possible)
- Disable file editing in dashboard
- Secure wp-config.php (400 permissions)
- Install security monitoring plugin
- Set up automated backups
- Schedule regular security scans
FAQ
Q: Is hiding the WordPress version enough for security? A: No. While it helps prevent automated attacks, determined attackers can still fingerprint WordPress through other means. It should be combined with proper hardening measures.
Q: Will removing version strings break my site?
Q: Can security headers break my site?
Q: Should I disable XML-RPC if I use Jetpack?
Q: What’s the most important security measure?
Q: How often should I scan for malware?
Q: Is 2FA really necessary?
Summary: The Layered Security Approach
Security is not a product; it is a process.
- Hide info (remove version strings) - Security through obscurity
- Harden connection (HSTS, Headers, CSP) - Browser-level protection
- Close doors (Disable XML-RPC, limit logins) - Reduce attack surface
- Lock files (Permissions, disable editing) - Contain breaches
- Monitor (Security plugins, logs) - Detect and respond
Do this, and you are safer than 99% of WordPress sites out there.
Remember: Security is layered. No single measure is perfect, but together they create a fortress. Start with the basics (updates, strong passwords), add obscurity (hide version), then implement hardening (headers, permissions), and finally monitoring.
The goal isn’t to be unhackable—that’s impossible. The goal is to be a harder target than the next site, causing attackers to move on to easier prey.


