Open any WordPress site in the world and append /readme.html to the URL. In the vast majority of cases you will get a clean HTML page that proudly announces the exact WordPress version in the headline. Same story for /license.txt, which is slightly less specific but still confirms that the site runs WordPress and gives a rough idea of how recent the install is.
Both files ship with WordPress core. They are restored on every update. Neither of them is needed for the site to work. The only audience they realistically serve is automated scanners that fingerprint sites by core version so they can match the result against a list of known vulnerabilities. This guide walks through why that matters in practice, and how to block the files cleanly on Apache, on nginx and on shared hosting where you have less direct control.
Why does it matter that readme.html is reachable?
The honest answer is: on its own, not very much. Knowing the WordPress version of a site is not a vulnerability. The version of WordPress core is not a secret, and a determined attacker can usually figure it out anyway, for example from generator meta tags, the version query parameter on enqueued scripts and styles, or from the structure of the REST API response.
What changes the picture is the way exploitation works at scale today. Mass scanners do not pick a target and then go look for bugs. They pick a CVE, build a list of every domain on the internet running an affected version, and fire the exploit at the entire list. The cheapest way to build that list is to fetch readme.html on millions of domains in parallel and parse the version out of the headline. Anything that makes the site invisible to that filter step removes you from the candidate list before the actual exploit ever runs.
So removing readme.html does not make the site secure. It removes one of the cheapest fingerprints, which in practice means you stop showing up in automated lists of "WordPress sites running version X.Y.Z". That is worth a few minutes of work even if it is not the most critical security hardening you can do.
What about license.txt and the other core files?
Same family of file, slightly less interesting:
license.txtcontains the GPL license text. It does not include a version number, but its existence at the WordPress root is a strong signal that the site runs WordPress.wp-config-sample.phpships with every install. It does not contain credentials, but reveals that no one has tidied up after setup.readme.htmlis the one with the version disclosure problem.
The block rule below covers all three in one go. There is no good reason to leave any of them publicly reachable.
Option 1: Block via .htaccess (Apache and LiteSpeed)
If your host runs Apache or LiteSpeed (which covers most shared hosters in the German speaking market: All Inkl, IONOS, Strato, DomainFactory, Hostinger and most resellers), you can add the following block to the .htaccess file in your WordPress root directory:
<FilesMatch "^(readme\.html|license\.txt|wp-config-sample\.php)$">
Require all denied
</FilesMatch>
The snippet uses Apache 2.4 syntax, which is what every host has been running for years. If you are stuck on Apache 2.2 (extremely rare in 2026, but possible on legacy hosting), use the older syntax:
<FilesMatch "^(readme\.html|license\.txt|wp-config-sample\.php)$">
Order allow,deny
Deny from all
</FilesMatch>
Place the block above the # BEGIN WordPress marker. Everything between # BEGIN WordPress and # END WordPress is managed by WordPress itself and gets rewritten when permalinks change or core updates run. Anything outside those markers is left alone.
After saving the file, open https://yourdomain.com/readme.html in a private browser window. You should get a 403 Forbidden. Same for /license.txt. If you still see the file contents, your host either ignores .htaccess entirely (rare on Apache, normal on nginx) or has AllowOverride set to a value that strips FilesMatch directives. Jump to Option 3.
Option 2: Block via nginx
On nginx there is no .htaccess. If you run your own server (a VPS at Hetzner, Netcup, DigitalOcean, or a managed nginx host where you control the config), add the following inside the server block of your site:
location ~* ^/(readme\.html|license\.txt|wp-config-sample\.php)$ {
deny all;
return 403;
}
Reload nginx with sudo nginx -t && sudo systemctl reload nginx, then test with curl -I https://yourdomain.com/readme.html. The first line of the response should read HTTP/2 403.
Several managed WordPress hosts that use nginx internally (Raidboxes, Kinsta, WP Engine, Cloudways) ship rules of this kind by default. Worth checking the host documentation. If they do not, support will usually add the rule for you on request.
Option 3: Shared hosting where .htaccess overrides do not stick
Some locked down shared hosts run nginx and ignore .htaccess entirely. Others run Apache but with restrictive AllowOverride rules. If neither Option 1 nor Option 2 is available, you have three reasonable fallbacks, sorted by how durable they are:
- Use a security plugin that handles core file hardening for you. Solid Security and All In One WP Security & Firewall both have a "remove core files" or "hide WordPress version" toggle that takes care of readme.html. Wordfence in Extended Protection mode can also block the files at the WAF layer. The advantage is that these settings survive WordPress updates automatically.
- Empty the file via a Must Use plugin. If a server level rule is impossible, the most reliable approach is a small mu plugin that overwrites readme.html and license.txt with empty content after every WordPress update. See the related code snippet article in our knowledge base.
- Delete the files manually. The fastest fix takes ten seconds via SFTP: just delete
readme.html,license.txtandwp-config-sample.phpfrom the WordPress root. The catch is that WordPress core updates restore the files. You need to remember to delete them again after every major update, which is why a plugin or webserver rule is the better long term answer.
What you should not do
A few approaches that show up on older blog posts but are worse than they look:
- Renaming the files. Renaming
readme.htmltoreadme.html.oldlooks tidy but does not help. WordPress just creates the original again on the next update, and now you have two files instead of one. - Setting file permissions to 000. This blocks reads on most hosts, but the next core update resets the permissions, and on some hosts it also breaks the upgrade itself if WordPress cannot read the file during the update process.
- Editing the files to remove the version number. Tempting but pointless. WordPress restores the original on update, and even an edited readme.html still confirms that the site runs WordPress.
The pattern is the same every time: anything that lives inside the WordPress install directory and is not protected by a webserver rule will get reset on update. Block at the webserver level, or use a plugin that knows how to keep itself active across updates.
How to verify your setup
- Open
https://yourdomain.com/readme.htmlin a private window. Expected result: a403 Forbiddenpage (or404if you went the deletion route). - Same check for
https://yourdomain.com/license.txtandhttps://yourdomain.com/wp-config-sample.php. - If you still see the file contents, clear any caches that sit in front of the site (Cloudflare, Varnish, server side page caches like LiteSpeed Cache or WP Rocket) and try again. Cached responses can hide the change for hours.
- Run a fresh InspectWP scan. The check for an exposed readme.html in the security section should turn green.
While you are at it
The same kind of FilesMatch or location rule is worth applying to a few other endpoints that show up in InspectWP scans regularly: wp-config.php.bak, wp-config.php.swp, .git/, .env, phpinfo.php and any info.php a developer left behind during debugging. Same mechanism, same five lines of config, and you remove a whole class of casual fingerprinting and credential leaks in one go.