Skip to content
Server & .htaccess

WordPress .htaccess Security Rules That Actually Help

Most of what gets sold as WordPress .htaccess security is padding. The rules that actually change your attack surface are short — here is which is which

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.