WordPress .htaccess Security Rules That Actually Help
Three .htaccess rules genuinely shrink your WordPress attack surface: block PHP in uploads, deny wp-config.php, disable directory indexing. Skip the rest.
Published
Most of what gets sold as “WordPress .htaccess security” is padding. The rules that actually change your attack surface are short: block PHP execution inside wp-content/uploads, deny direct access to wp-config.php, and disable directory indexing. The popular ones — blocking xmlrpc.php “to stop brute force,” hiding your WordPress version, pasting a 200-line bad-bot user-agent list — range from marginal to pure theatre. Below is which is which, and why.
One thing to settle first: .htaccess only does anything on Apache (and LiteSpeed, which reads it too). On nginx it is ignored entirely — the file just sits there while you believe you’re protected. If your host runs nginx, none of this applies and you need server/location blocks instead. Check with curl -I https://yoursite.com and look at the Server: header before you spend an hour editing a file the server never reads.
The rule that actually matters: no PHP in /uploads
This is the one worth doing. wp-content/uploads is world-writable by design — every media upload, every plugin that saves a file, writes there. If an attacker gets a .php file into that directory (through a vulnerable upload handler, an image field that doesn’t validate MIME type, a compromised plugin), the difference between an annoyance and a full remote-code-execution compromise is whether the server will execute that file when requested. Deny execution and the uploaded payload is just an inert file sitting on disk.
Drop this into wp-content/uploads/.htaccess (create the file if it doesn’t exist):
<FilesMatch "\.php$">
Require all denied
</FilesMatch>
That is Apache 2.4 syntax. On the older 2.2, the equivalent is Order Deny,Allow / Deny from all. Mixing the two dialects in one file is the most common cause of a sudden 500 Internal Server Error after “hardening” — if the whole site dies the moment you save, that’s almost always the reason. Check your version with apachectl -v.
Protect wp-config.php
wp-config.php holds your database credentials and auth salts. While PHP is running, a direct request to it returns a blank page — PHP executes the file instead of printing it. The risk is the failure case: if PHP ever crashes, gets misconfigured during a migration, or the handler is disabled, Apache serves the file as plaintext and dumps your DB password to anyone who asks. Denying access is cheap insurance against a bad five minutes:
<Files wp-config.php>
Require all denied
</Files>
Turn off directory indexing
If someone visits a folder with no index.php and Apache has Options +Indexes on, it lists the contents — every backup file, every stray SQL dump you forgot about. Turn it off site-wide:
Options -Indexes
Low severity, but real, and it costs nothing.
That’s the honest core list. You can assemble these — plus the correct 2.4-vs-2.2 syntax so you don’t 500 your site — with the WordPress .htaccess generator rather than copy-pasting from a forum post written for the wrong Apache version.
What NOT to do
Blocking xmlrpc.php “for brute-force protection.” This is the big one everybody repeats and it’s wrong as stated. Yes, XML-RPC’s system.multicall method historically let an attacker bundle many login guesses into a single request — real amplification. But the overwhelmingly common brute-force vector is plain POST requests to wp-login.php, and blocking xmlrpc.php does nothing for that. Brute force is defeated by rate limiting, strong passwords, and 2FA — not by killing one endpoint. There is a legitimate reason to disable XML-RPC: its pingback feature can be abused for DDoS reflection, so if you don’t use Jetpack, the mobile app, or pingbacks, closing it shrinks your attack surface. Just don’t tell yourself it’s your brute-force defense, because it isn’t.
Hiding your WordPress version / removing the generator tag. Stripping readme.html and the <meta name="generator"> tag feels like hardening. An attacker fingerprints your version from enqueued asset query strings, block-editor markup, and a dozen other tells in seconds. You’re not hiding anything; you’re just making yourself feel busy.
Giant bad-bot user-agent and referrer blocklists. User agents are a single spoofed HTTP header. These lists are stale the day you paste them, they block nothing competent, and Apache evaluates every regex on every request — you’re paying a real performance tax for zero security. Skip them.
IP-locking wp-login.php. Great until your ISP rotates your address and you’ve locked yourself out of your own admin. Only viable with a genuinely static IP.
Author-enumeration redirects (?author=1). The .htaccess rewrite people paste for this is incomplete on its own — the REST endpoint /wp-json/wp/v2/users still lists usernames. Blocking one path while the other stays open is theatre.
Still stuck?
If a rule 500s the site, it’s syntax — pull the last block you added and reload; that isolates it immediately. If a rule seems to do nothing, confirm you’re actually on Apache and that AllowOverride is enabled for the directory (many managed hosts restrict it). Build the file from a known-good template with the .htaccess generator, keep the three rules that matter, and drop the rest.
FAQ
Questions
How do I stop PHP files from running in the WordPress uploads folder?
Create a .htaccess file inside wp-content/uploads with a FilesMatch block matching .php files and Require all denied. Because uploads is world-writable by design, denying execution means a malicious .php file that gets in there sits on disk as an inert file instead of becoming remote code execution.
Do I need to block direct access to wp-config.php?
Yes, and it costs nothing. Add a Files block for wp-config.php with Require all denied. While PHP is running, a direct request just returns a blank page, but if PHP crashes, gets misconfigured during a migration, or the handler is disabled, Apache serves the file as plaintext and dumps your database credentials.
Why did my site show a 500 Internal Server Error right after I edited .htaccess?
It is almost always mixed Apache syntax. Require all denied is Apache 2.4, while Order Deny,Allow with Deny from all is 2.2, and combining both dialects in one file breaks the site the moment you save. Check your version with apachectl -v, or remove the last block you added and reload.
Does .htaccess work on nginx?
No. .htaccess only does anything on Apache, and on LiteSpeed, which reads it too. On nginx the file is ignored entirely, so it sits there while you believe you are protected. Run curl -I on your site and check the Server: header first, then use server or location blocks instead.
Does blocking xmlrpc.php stop WordPress brute force attacks?
No. The overwhelmingly common brute-force vector is plain POST requests to wp-login.php, which blocking xmlrpc.php does nothing about. Brute force is defeated by rate limiting, strong passwords, and 2FA. Disabling XML-RPC is still worth it if you do not use Jetpack, the mobile app, or pingbacks, since pingback can be abused for DDoS reflection.