Every WordPress plugin ships with a readme.txt file that publicly exposes its exact version number, tested WordPress compatibility range, and sometimes even known upgrade paths — all readable by anyone with a browser. Attackers routinely scan for these files to fingerprint plugin versions and cross-reference them against public vulnerability databases before launching targeted exploits.
What Information Does readme.txt Actually Expose?
A readme.txt file exposes structured metadata that attackers can parse programmatically. The file lives in every plugin directory (e.g. /wp-content/plugins/woocommerce/readme.txt) and is publicly accessible by default on virtually every WordPress installation unless deliberately blocked.
Here's what a typical readme.txt header block looks like:
=== WooCommerce ===
Contributors: automattic, mikejolley, jameskoster
Tags: e-commerce, store, sales, sell, shop
Requires at least: 6.4
Tested up to: 6.7
Requires PHP: 7.4
Stable tag: 9.4.2
License: GPLv3
That Stable tag line is the critical one. It tells an attacker exactly which version is installed. Combined with the Requires at least and Tested up to fields, it narrows the vulnerability window further — an attacker knows the site is running a version between those two bounds even if Stable tag is stale.
Beyond the header, the readme.txt changelog section is often even more revealing. Developers document exactly what security issues were patched and in which version:
= 9.3.1 =
* Fix - Prevent unauthenticated access to order metadata endpoint.
If the installed version predates that patch note, the attacker now has a precise exploit description and knows the target is vulnerable.
How Attackers Use readme.txt in Practice
Reconnaissance is the first phase of any targeted attack, and plugin fingerprinting is cheap and fast. Tools like WPScan, Nuclei, and custom Python scripts can enumerate hundreds of plugin readme.txt files in a single HTTP GET sweep. According to Sucuri's 2024 Website Threat Research Report, outdated plugins and themes account for 36% of all WordPress compromises — and automated scanners are the primary discovery mechanism.
The attack flow looks like this:
- Scanner sends
GET /wp-content/plugins/{plugin-name}/readme.txtfor a list of common plugins - Parses
Stable tagfrom the response - Cross-references the version against WPVulnDB, NVD, or Exploit-DB
- If a known CVE exists for that version, the site is flagged for exploitation
- Exploit payload is sent — often within the same automated session
This entire chain can complete in under 30 seconds per target. There is no human in the loop during reconnaissance. The readme.txt file is doing the attacker's work for them.
The same logic applies to readme.txt files in themes (/wp-content/themes/{theme}/readme.txt) and the WordPress core files (/readme.html, which exposes the WordPress version number directly).
What Files Should You Block?
readme.txt is the highest-priority target, but it's not the only disclosure file worth restricting. Here's a complete inventory:
| File | Location | What It Reveals |
|---|---|---|
readme.txt | Every plugin/theme directory | Exact plugin/theme version, changelog with patch notes |
readme.html | WordPress root | WordPress core version number |
license.txt | WordPress root | Confirms WordPress installation |
wp-config-sample.php | WordPress root | Database configuration patterns |
CHANGELOG.md / CHANGELOG.txt | Some plugin directories | Detailed patch history |
package.json | Some plugin directories | Dependency versions, build tool info |
composer.json | Some plugin directories | PHP dependency versions |
.git/config | If version-controlled in webroot | Repository URL, branch names |
phpinfo.php | If left by developer | Full PHP environment disclosure |
The last two — .git/config and phpinfo.php — represent particularly severe exposures if present. A readable .git directory means your entire source history is potentially downloadable.
How to Block readme.txt in NGINX
For NGINX-based servers, blocking readme.txt and related files is a location block directive. Add this inside your server block:
# Block WordPress version disclosure files
location ~* /wp-content/.*\.(txt|md|html|json|lock)$ {
deny all;
return 404;
}
location ~* ^/(readme\.html|license\.txt|wp-config-sample\.php)$ {
deny all;
return 404;
}
# Block .git directory access entirely
location ~ /\.git {
deny all;
return 404;
}
Why return 404 instead of return 403? Returning a 403 Forbidden confirms to the attacker that the file exists. A 404 gives no useful information about whether the resource is present. This matters more for .htaccess and wp-config.php protection than for readme.txt, but it's a good habit to apply uniformly.
If you want a more targeted rule that only catches readme.txt specifically:
location ~* /readme\.(txt|html)$ {
deny all;
return 404;
}
After editing, always test your NGINX config before reloading:
nginx -t && systemctl reload nginx
How to Block readme.txt in Apache
For Apache servers using .htaccess, add these directives to your WordPress root .htaccess file (above the standard WordPress rewrite rules):
# Block WordPress disclosure files
<FilesMatch "^(readme\.(txt|html)|license\.txt|wp-config-sample\.php)$">
Order Allow,Deny
Deny from all
</FilesMatch>
# Block readme.txt files in plugin/theme directories
<FilesMatch "readme\.(txt|md|html)$">
Order Allow,Deny
Deny from all
</FilesMatch>
# Block .git directory
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^\.git - [F,L]
</IfModule>
On Apache 2.4+, use the newer Require syntax instead:
<FilesMatch "^(readme\.(txt|html)|license\.txt)$">
Require all denied
</FilesMatch>
Note that .htaccess rules only apply if AllowOverride is enabled for that directory in your Apache virtual host configuration. On many optimized stacks, AllowOverride is disabled (for performance), meaning these rules will be silently ignored. If you're on a managed host, verify the rules are actually taking effect using a curl test:
curl -I https://yourdomain.com/wp-content/plugins/woocommerce/readme.txt
# Should return HTTP/1.1 404
WordPress-Level Protection with wp-config.php
While server-level blocking is the correct place to handle this, you can add a secondary layer inside WordPress itself using wp-config.php or a custom plugin. This approach won't beat server-level configuration for performance, but it catches cases where server rules aren't applied consistently across a multisite network or subdirectory install.
Add this to wp-config.php before the /* That's all, stop editing! */ line:
// Redirect sensitive files to 404
add_action('init', function() {
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
if (preg_match('/readme\.(txt|html)$/i', $request_uri)) {
status_header(404);
exit;
}
});
This is a fallback, not a replacement. If the request reaches PHP, it's already consumed server resources. Block it at the web server layer first.
Security Plugin Alternatives
If you're not comfortable editing server config files directly, security plugins can handle this automatically:
| Plugin | Method | Covers readme.txt | Covers .git | Performance Cost |
|---|---|---|---|---|
| Wordfence | PHP-level blocking | ✅ | ✅ | Medium (request still hits PHP) |
| iThemes Security | .htaccess injection | ✅ | ✅ | Low (if Apache) |
| WP Cerber | PHP + .htaccess | ✅ | Partial | Medium |
| All-In-One WP Security | .htaccess injection | ✅ | ✅ | Low (if Apache) |
| Manual server config | NGINX/Apache native | ✅ | ✅ | Zero |
The manual server configuration approach wins on performance because the request never hits PHP. The security plugin approach is a reasonable fallback for shared hosting environments where you don't control the web server configuration.
Before adding any security plugin, do a WordPress plugin audit to make sure you're not compounding plugin bloat with another heavyweight dependency for something a three-line NGINX rule can handle.
The Bigger Picture: Version Disclosure Is Not Just a readme.txt Problem
Blocking readme.txt is a quick win, but it's one piece of a broader version disclosure problem. Attackers have multiple fallback fingerprinting methods:
- HTTP response headers — Some plugins inject
X-Powered-ByorX-Plugin-Versionheaders - HTML source comments — Plugin enqueue functions often include version query strings (
?ver=9.4.2) - REST API responses — The WordPress REST API can expose generator information at
/wp-json/ - RSS feed — WordPress includes a generator tag in RSS feeds:
<generator>https://wordpress.org/?v=6.7</generator>
The asset version query string approach is particularly common. When a plugin enqueues a stylesheet like /wp-content/plugins/woocommerce/assets/css/frontend.css?ver=9.4.2, that version number is visible in the page source without touching readme.txt at all.
To remove version query strings from enqueued assets:
// functions.php or a custom plugin
add_filter('style_loader_src', 'remove_version_query_strings', 10, 2);
add_filter('script_loader_src', 'remove_version_query_strings', 10, 2);
function remove_version_query_strings($src, $handle) {
if (strpos($src, '?ver=') !== false) {
$src = remove_query_arg('ver', $src);
}
return $src;
}
Note: This can cause cache-busting issues if you're not managing asset versioning through another mechanism. It's a tradeoff — version stripping improves security hardening but may require you to manually invalidate CDN caches after plugin updates.
For the RSS generator tag, add this to functions.php:
remove_action('wp_head', 'wp_generator');
add_filter('the_generator', '__return_empty_string');
This pairs well with understanding which plugins are actually still supported — the WordPress plugin directory removal security problem is closely related: abandoned plugins stop receiving security patches, and exposed readme.txt version data makes it trivially easy for scanners to identify sites still running them.
How Managed Hosting Handles This
On managed WordPress infrastructure like TopSyde's managed WordPress hosting, server-level hardening is applied at provisioning time. Every site gets NGINX rules that block readme.txt, readme.html, license.txt, wp-config-sample.php, and .git directory traversal without any manual configuration. This is part of the what managed WordPress hosting actually includes — not an add-on or a premium tier feature.
Beyond static file blocking, AI-powered WordPress monitoring can detect when new disclosure vectors appear — for example, when a plugin update introduces a new CHANGELOG.md or package.json that wasn't there before. Static server rules cover known patterns; monitoring catches new ones after plugin updates.
If you want to audit your current security posture before migrating or making configuration changes, the TopSyde spec sheet documents exactly which server-level hardening rules are applied by default.
Verification: Testing Your Configuration
After applying any of the above rules, verify they're working:
# Test readme.txt in a plugin directory
curl -o /dev/null -s -w "%{http_code}" \
https://yourdomain.com/wp-content/plugins/woocommerce/readme.txt
# Test WordPress root readme.html
curl -o /dev/null -s -w "%{http_code}" \
https://yourdomain.com/readme.html
# Test .git directory
curl -o /dev/null -s -w "%{http_code}" \
https://yourdomain.com/.git/config
All three should return 404. If any return 200 or 403, your blocking rules aren't applied correctly. A 403 means the file exists and the server confirmed it — go back and change your rules to return 404 instead.
You can also run WPScan against your own site (with authorization) to see what a scanner sees:
wpscan --url https://yourdomain.com --enumerate p --plugins-detection passive
If WPScan can enumerate plugin versions from readme.txt files, your blocking rules aren't working.
Frequently Asked Questions
Does blocking readme.txt break plugin functionality?
No. readme.txt files are documentation for the WordPress.org plugin directory — they are not loaded or read by WordPress at runtime. Plugins do not require their readme.txt to be publicly accessible for any functionality. Blocking them at the web server level has zero impact on plugin behavior.
Should I delete readme.txt files instead of blocking them?
Blocking is better than deleting. If you delete readme.txt files, plugin updates will restore them automatically — you'd need to re-delete after every update. A server-level block rule applies permanently regardless of what files plugins add or restore during updates.
Does this protection help if a plugin has an unpatched zero-day vulnerability?
Not directly — blocking readme.txt prevents version fingerprinting, which makes automated mass scanning less effective. However, if an attacker already knows which plugins your site uses through other means (HTML source comments, asset URLs, or prior reconnaissance), blocking readme.txt won't prevent a targeted exploit. Defense in depth requires combining file disclosure blocking with WordPress security best practices, keeping plugins updated, and running active monitoring.
Can Google or other crawlers access readme.txt files?
Yes, before you block them — and that can actually expose version data in cached Google results. After blocking, Googlebot will receive a 404 and eventually drop any cached versions. There is no SEO benefit to Googlebot accessing readme.txt files; they contain no content valuable for indexing.
What about readme.txt files in the WordPress core itself?
WordPress ships a readme.html (not .txt) in the root directory that exposes the WordPress core version. This should be blocked with the same server rules. The license.txt in the WordPress root also
Topics

DevOps & Security Lead
12+ years DevOps, Linux & cloud infrastructure certified
Marcus leads infrastructure and security at TopSyde, managing the server fleet and AI monitoring systems that keep client sites fast and protected. Former sysadmin turned WordPress hosting specialist.



