There’s a specific kind of dread that comes from editing .htaccess. You paste in a rule you found somewhere, hit save, refresh the site — and instead of your homepage you get a stark white page that says 500 Internal Server Error. No dashboard, no login, nothing. Just a broken site and a rising heartbeat.
I’ve watched people do this at 2 a.m. before a product launch. So before we get into a single security rule, I want to promise you two things: I’ll explain what every rule actually does and what can break, and I’ll show you exactly how to recover when something goes wrong. Because it will, eventually, and knowing the rollback turns a catastrophe into a two-minute fix.
.htaccess is a genuinely useful WordPress security tool. Used well, it blocks access to sensitive files, stops PHP from running where it shouldn’t, adds protective headers, and quietly turns away a lot of low-effort attacks before they ever reach WordPress. But let me be blunt about its limits right up front, because this is the mistake I see most:
.htaccesscan harden your WordPress installation, but it cannot replace updates, secure authentication, backups, least privilege, malware monitoring, and proper server security. It’s one layer in a stack, not the stack itself.
Treat it as a lock on specific doors, not a security system for the whole building. With that framing set, let’s make those locks solid.
What you’ll get from this guide
A rule-by-rule walkthrough of practical .htaccess hardening — each one explained as what it does → why it matters → the risk → when to use it → how to test → how to undo it. Plus the compatibility gotchas (Apache versions, LiteSpeed, Nginx), a WooCommerce-specific caution section, a full testing checklist, troubleshooting for the dreaded 500, and a conservative baseline you can actually trust. No 100-rule copy-paste monsters. No “this makes you unhackable.” Just the stuff that works and the honest caveats about when it doesn’t.
What is .htaccess?
.htaccess (the name literally means “hypertext access”) is a per-directory configuration file for the Apache web server. When Apache receives a request, it reads the .htaccess file in the relevant directory (and its parents) and applies whatever directives it finds before handing the request off to WordPress. That “before WordPress” part is exactly why it’s useful for security — it can reject a malicious request while PHP and WordPress are still asleep.
In plain terms, .htaccess lets you:
- Rewrite URLs — this is how WordPress “pretty permalinks” work.
- Restrict access — deny requests to specific files or from specific sources.
- Set HTTP headers — including security headers.
- Redirect requests — HTTP to HTTPS, old URLs to new.
- Block requests — by file type, method, user agent, or pattern.
- Control server behavior — directory listing, PHP handling, and more.
It helps to place .htaccess alongside the other things people confuse it with:
| Mechanism | What it is | Where it lives | What it controls |
|---|---|---|---|
.htaccess | Apache per-directory config | In your web directories | Request handling, access, headers, rewrites |
wp-config.php | WordPress configuration | WordPress root | DB credentials, secrets, WordPress constants |
| WordPress settings | App-level options | Database | Site behavior, users, plugins |
| PHP configuration | PHP runtime settings | php.ini / .user.ini | How PHP itself behaves |
| WAF | Web Application Firewall | Edge or server | Filtering malicious traffic by rules/behavior |
| Apache server config | Main Apache config | httpd.conf / vhost | Server-wide behavior (overrides .htaccess) |
One crucial accuracy point before we go further: .htaccess is an Apache thing. It does not work the same way everywhere:
- Apache — full support; this is
.htaccess‘s native home. - LiteSpeed — supports most Apache-compatible
.htaccessdirectives (it’s designed for drop-in compatibility), but some behaviors are server-specific, so test. - Nginx — does not use
.htaccessat all. If you’re on Nginx-only hosting, none of these rules apply; the equivalent logic lives in the server config. Many “why isn’t my.htaccessrule working?” mysteries come down to the site running on Nginx.
If you’re not sure which server you’re on, check your hosting panel or ask your host. It determines whether this entire guide applies to you directly, applies with caveats (LiteSpeed), or applies conceptually only (Nginx).
Where is the WordPress .htaccess file?
For a standard Apache/LiteSpeed WordPress install, the main .htaccess lives in your WordPress root — the same folder as wp-config.php and index.php:
public_html/
├── .htaccess ← the main one
├── wp-admin/
├── wp-content/
├── wp-includes/
├── wp-config.php
├── index.php
└── wp-login.php
There can be others too — WordPress and some plugins create .htaccess files in subdirectories (like wp-content/uploads/ or for caching). But the root one is where the action is.
Here’s why you might not see it: the leading dot makes it a hidden file on Unix-like systems, so file managers and FTP clients hide it by default. To find it:
- cPanel File Manager — click Settings (top right) and enable Show Hidden Files (dotfiles).
- FTP/SFTP clients (FileZilla, etc.) — enable “show hidden files” in the view/server menu.
- Other hosting panels — look for a similar “show hidden/dotfiles” toggle.
If you genuinely have no .htaccess at all, it may be because your permalinks are set to “Plain” (WordPress only writes the rewrite block for pretty permalinks) or because you’re on Nginx. On Apache, simply saving your permalink settings (Settings → Permalinks → Save) regenerates it.
Back up .htaccess before you touch it
I’m putting this early and in its own section because it’s the difference between “oops” and “disaster.” Before you change a single character, make a safe copy you can restore.
The safe editing workflow:
- Download a copy of the current working
.htaccessto your computer. Name it something obvious likehtaccess-working-backup.txt. - Keep that known-good version somewhere you won’t lose it.
- Make one change at a time. Not five. One.
- Test the site after each change (homepage, a post, wp-admin — the full checklist comes later).
- Keep hosting File Manager or SFTP access open in another tab, ready to intervene.
- Know your restore procedure cold before you need it.
Because here’s what happens when a rule is bad: the whole site can return HTTP 500 Internal Server Error — every page, including your login. .htaccess errors are not graceful. A single unsupported directive or typo takes everything down.
Your emergency rollback, memorized:
- Open File Manager (or connect via SFTP) — this doesn’t depend on WordPress, so it works even when the site is 500-ing.
- Rename the broken
.htaccessto.htaccess-broken. - Refresh the site. If it loads, the
.htaccesswas the culprit (confirmed). - Restore your known-good backup as
.htaccess. - Re-add rules one at a time, testing after each, to find the offender.
Warning: Never edit
.htaccesson a live site without confirmed File Manager or SFTP access first. If your only way in is wp-admin and a bad rule locks you out of wp-admin too, you’re stuck calling your host. Confirm your escape route before you start.
Keep this rollback in mind through every rule below. It’s what makes experimenting safe.
Understand the default WordPress rules first
WordPress generates its own .htaccess block for permalinks, and you need to understand it so you don’t accidentally destroy it. Here’s the standard block:
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
Line by line:
# BEGIN WordPress/# END WordPress— WordPress manages everything between these markers and may rewrite it. Put your custom rules outside this block, or WordPress can overwrite them.<IfModule mod_rewrite.c>— only run these rules if Apache’s rewrite module is loaded (graceful fallback if it isn’t).RewriteEngine On— turns on URL rewriting.RewriteBase /— sets the base path for rewrites.RewriteRule ^index\.php$ - [L]— if the request is literallyindex.php, stop and serve it (don’t rewrite).RewriteCond %{REQUEST_FILENAME} !-f— the following rule applies only if the requested thing is not a real file.RewriteCond %{REQUEST_FILENAME} !-d— …and not a real directory.RewriteRule . /index.php [L]— everything else (a “pretty” URL that isn’t a real file/folder) gets routed toindex.php, which is how WordPress interprets it.
The takeaway: this block is what makes /my-post-name/ work instead of /?p=123. Add your security rules above # BEGIN WordPress or below # END WordPress, never inside, and never blindly paste a downloaded .htaccess that replaces this section — you’ll break your permalinks and possibly the whole site.
Disable directory browsing
What it does: stops Apache from showing a clickable file listing when a directory has no index file.
# Disable directory listing
Options -Indexes
Why it matters. Without this, visiting a folder that lacks an index.php/index.html — say https://example.com/wp-content/uploads/2026/07/ — can show Apache’s auto-generated listing of every file in that folder. That hands an attacker a free map: your upload structure, plugin folders, backup files you forgot about, naming patterns. It’s reconnaissance you’re giving away for nothing.
The risk: essentially none. This is one of the safest hardening rules there is.
When to use it: virtually always. Many hosts even enable it by default.
How to test: visit a directory with no index file (an uploads subfolder works). Before, you might see a file list; after, you should get a 403 Forbidden.
How to undo it: remove the line, or change it to Options +Indexes.
Security Note: Disabling directory browsing hides the file list — it doesn’t secure the files. Someone who guesses or already knows a direct URL can still request that specific file. Hiding the listing and actually protecting files (next sections) are different jobs. Do both.
Protect wp-config.php
What it does: blocks direct HTTP access to wp-config.php.
# Protect wp-config.php (Apache 2.4+)
<Files "wp-config.php">
Require all denied
</Files>
Why it matters. wp-config.php is the most sensitive file in your install — it holds your database name, username, password, and host, plus your authentication keys and salts. Normally PHP executes it and outputs nothing, so a direct request returns a blank page. But if the PHP handler ever fails (a broken deploy, a server misconfiguration), Apache could serve the file’s raw contents as text — credentials and all. This rule ensures that even in that failure scenario, the request is denied outright.
The risk: low, but watch the syntax. The Require all denied form is Apache 2.4+. On old Apache 2.2 you’d need the legacy Order allow,deny / Deny from all syntax instead (more on that mismatch later). On most modern hosting, 2.4 syntax is correct.
When to use it: almost always. It’s a small, high-value safety net.
How to test:
curl -I https://example.com/wp-config.php
You want 403 Forbidden (or 404), never a 200 that returns file content.
How to undo it: remove the <Files "wp-config.php"> block.
Security Note: This blocks the file over HTTP. It does nothing about filesystem-level reads, a leftover
wp-config.php.bak, or a Git leak..htaccessprotection of this file is one layer among several — the dedicated wp-config hardening guide covers the rest.
Protect .htaccess itself
What it does: prevents the .htaccess file from being downloaded over HTTP.
# Protect .htaccess and .htpasswd files
<Files ~ "^\.ht">
Require all denied
</Files>
Why it matters. Your .htaccess reveals your security rules, blocked paths, redirect logic, and sometimes IPs. Handing an attacker your ruleset tells them exactly what you’re defending and how to route around it. This rule denies access to any file starting with .ht (covering .htaccess and .htpasswd).
The risk: minimal. Most Apache installs already deny .ht* files in the server config by default — this makes it explicit at the directory level.
When to use it: safe to include as belt-and-suspenders. Just know that server-level config may already handle this, and in some cases can override or restrict directory-level behavior.
How to test: curl -I https://example.com/.htaccess — expect 403.
How to undo it: remove the block.
Protect sensitive WordPress files (deliberately)
Beyond wp-config.php and .htaccess, a few other files may deserve protection depending on your setup:
.user.iniandphp.ini— PHP configuration that can reveal settingserror_log— can leak paths and internal details- backup archives and database exports (covered next)
- stray configuration and temporary files
You can protect specific ones like this:
# Protect specific sensitive files (add only what applies to your site)
<FilesMatch "^(\.user\.ini|php\.ini|error_log|wp-config\.php\.bak)$">
Require all denied
</FilesMatch>
But here’s the principle that matters more than the snippet:
Block sensitive files deliberately — don’t use a reckless blanket rule. It’s tempting to “just block everything starting with a dot” or every file with certain extensions, but legitimate WordPress and server behavior varies. Some hosts rely on
.user.inifor PHP settings; some plugins use files you might not expect. Overly broad rules cause weird, hard-to-diagnose breakage. Name what you’re protecting.
How to test: request each protected file and confirm a 403, then confirm your site still functions normally (especially anything that relies on PHP settings).
How to undo it: remove the specific entries from the FilesMatch.
Block access to backup and archive files
What it does: denies HTTP access to common backup/archive file types.
# Deny access to backup and archive files
<FilesMatch "\.(bak|old|save|swp|orig|sql|zip|tar|gz|tgz|rar)$">
Require all denied
</FilesMatch>
Why it matters. This is one of the most common real-world breaches, and it requires zero exploitation. Files like backup.zip, database.sql, site-backup.tar.gz, old-site.zip, or wp-config.php.bak sitting in a public web directory can be downloaded directly by anyone who guesses the name — and attackers run automated scanners that check these exact names on thousands of sites. A public database.sql hands over your entire database. A wp-config.php.bak hands over your credentials, served as plain text (because the server doesn’t run .bak through PHP).
The risk: low for the rule itself, but make sure you’re not legitimately serving any of these extensions (rare, but check if you distribute .zip downloads — you’d need to carve out an exception or store those elsewhere).
When to use it: widely applicable as a safety net.
How to test: try requesting a test file with a blocked extension and confirm 403. Better yet, audit your web root for any archives that shouldn’t be there.
How to undo it: remove the FilesMatch block.
Security Note: This rule is a safety net, not the actual fix. The real best practice is to never store backups inside the public web root at all. Keep them outside
public_htmlor off-server entirely. The.htaccessrule protects you from the backup you forgot to move; not storing them publicly protects you from the rule you forgot to add.
Stop PHP execution in the uploads directory
This is one of the single most valuable hardening steps in this entire guide, so give it real attention.
What it does: prevents any PHP file inside wp-content/uploads/ from being executed, even if one somehow gets there.
Create a separate .htaccess file inside wp-content/uploads/ with:
# Place in wp-content/uploads/.htaccess
# Prevent execution of PHP files in the uploads directory
<Files "*.php">
Require all denied
</Files>
Why it matters. Your uploads directory holds media — images, PDFs, videos. It should never contain executable PHP. But upload-handling is a common vulnerability class: if an attacker exploits a flaw in a plugin’s file upload to sneak a .php file into uploads, and the server executes it, a mere file-upload bug becomes remote code execution — a web shell, and full site compromise. This rule breaks that chain: even if the malicious file lands, it can’t run. It becomes an inert file instead of a live backdoor.
The risk: low for typical sites, but not zero. Some plugins do unusual things involving PHP in upload paths (rare, but it happens), so test afterward. The exact effective directive can also depend on your Apache/PHP handler configuration — on some setups you may need an alternative approach, and on Nginx this doesn’t apply at all.
When to use it: almost always, for typical media-only upload directories.
How to test: after adding it, confirm your site still uploads and displays media normally. If you want to verify the block itself, a harmless test PHP file placed in uploads should return 403 rather than executing.
How to undo it: delete the .htaccess you created in wp-content/uploads/.
Warning: This does not by itself “prevent file upload attacks.” It prevents execution of uploaded PHP, which neutralizes the most dangerous outcome — but the underlying upload vulnerability still needs patching. Treat this as damage-limitation that pairs with keeping plugins updated, not a substitute for it.
Block direct access to PHP files in sensitive directories
Some directories that mostly hold includes or assets don’t need their PHP files reachable directly over HTTP. Restricting direct access can reduce the chance of a vulnerable script being invoked straight from the browser.
The catch is that this is exactly where over-aggressive rules break WordPress. Plugins and themes legitimately expose PHP endpoints in various places, and WordPress’s own architecture routes things in ways that a broad “block all PHP here” rule can shatter.
So the honest guidance: this is an advanced, site-specific technique, not a broad recommendation. If you understand your specific plugin/theme’s file usage and have tested thoroughly on staging, targeted restrictions can help. Applied blindly across wp-includes/ or wp-content/ wholesale, they will cause mysterious, hard-to-trace failures.
Warning: Aggressive PHP blocking is a leading cause of “my site half-works now” bugs. Symptoms are often subtle — a broken widget, a failing AJAX action, a plugin that silently stops working — rather than an obvious 500. If you go down this road, do it on staging, change one directory at a time, and test the full site (especially AJAX, REST, and plugin features) after each change.
Disable XML-RPC — when you actually don’t need it
What it is. xmlrpc.php is a legacy remote-procedure interface that lets external applications talk to WordPress. It historically powered remote publishing, the WordPress mobile app, and pingbacks.
Why it’s a security topic. XML-RPC has been abused for brute-force amplification (bundling many login attempts into one request) and pingback-based attacks. It’s also non-interactive, so it can’t present a CAPTCHA or 2FA prompt. For a site that doesn’t use it, it’s pure attack surface.
But — and this matters — some sites genuinely need it. Jetpack, the mobile app, and certain integrations rely on XML-RPC. Blindly disabling it can silently break those.
You have a few approaches:
Block it at the server level (if nothing needs it):
# Block all access to xmlrpc.php
<Files "xmlrpc.php">
Require all denied
</Files>
Or disable it through WordPress (a filter in code, or a security plugin toggle) — useful when you want WordPress-aware control.
Or block only specific methods (like pingbacks) while keeping legitimate functionality — the more surgical option for sites that need some XML-RPC.
How to test: after blocking, verify anything that depends on XML-RPC still works — Jetpack connection, mobile app, remote publishing. curl -I https://example.com/xmlrpc.php should return 403 once blocked.
How to undo it: remove the <Files "xmlrpc.php"> block.
Security Note: Don’t disable XML-RPC reflexively because a checklist said so. Confirm nothing depends on it first. Modern WordPress has already curtailed the worst brute-force amplification, so for many sites this is optional hardening rather than urgent. Decide based on what your site actually uses.
Restrict access to wp-login.php
What it does: limits who can even reach the login page, by IP.
# Restrict wp-login.php to specific IPs (Apache 2.4+)
<Files "wp-login.php">
Require ip 203.0.113.10
# Add more with additional "Require ip" lines
</Files>
Why it matters. If only your IP can load wp-login.php, brute-force bots can’t even attempt a login — they get a 403 before WordPress processes anything. For the right site, it’s extremely effective.
The big caveat: this is impractical for most sites. It works beautifully when:
- You have a static office IP or a VPN with a fixed exit IP.
- There’s a small, fixed set of administrators.
It falls apart when:
- You’re on a dynamic residential IP that changes (you’ll lock yourself out).
- You have many administrators in different locations.
- You’re behind cloud hosting or a CDN where the “client IP” Apache sees may be a proxy’s, not the real user’s (this can block everyone or no one unexpectedly).
How to test: from an allowed IP, confirm you can log in. From a different network (phone on cellular, say), confirm you get 403.
How to undo it: remove the block — do this immediately if you find yourself locked out (via File Manager/SFTP).
Warning: Replace
203.0.113.10with your actual IP. Copying an example IP verbatim will lock you out of your own site. And behind Cloudflare or a load balancer, the IP Apache sees is often the proxy’s, so IP restrictions may not behave as you expect — test carefully before relying on them.
Protect wp-admin
There’s a useful distinction to draw here:
- Protecting the login page (
wp-login.php) — controls who reaches the login form. - Protecting
/wp-admin/— controls who reaches the entire admin area. - WordPress authentication — WordPress’s own login (username/password/2FA).
- Server-level authentication — an additional HTTP auth prompt (via
.htpasswd) before WordPress even loads.
That last option — HTTP Basic Auth on /wp-admin/ — adds a second, server-level password prompt in front of the WordPress login. An attacker has to get past two gates, and the outer one is handled by Apache before any WordPress code runs, which blunts attacks against WordPress itself.
When it’s appropriate:
- Development and staging sites — keep the whole admin private.
- Internal or private admin portals — where every legitimate user can be given the extra credentials.
When it’s inconvenient:
- Public sites with customer logins — WooCommerce customers and members hit
/wp-admin/-adjacent flows, and an HTTP auth prompt in front of that is confusing and breaks things. (Note that AJAX viaadmin-ajax.phplives underwp-admin/, so a blanket auth on the whole directory can break front-end AJAX — a classic footgun.)
How to test: confirm the HTTP auth prompt appears for /wp-admin/, that you can get through it, and — critically on a public site — that front-end AJAX and customer flows still work.
How to undo it: remove the auth directives (and the .htpasswd reference).
Warning: Applying HTTP auth to the entire
wp-admin/directory on a public WooCommerce or membership site frequently breaksadmin-ajax.php-based features. If you protectwp-admin, either scope it carefully or make sure you’ve tested every front-end feature that uses AJAX.
Block suspicious HTTP methods
Every request uses an HTTP method: GET (fetch), POST (submit), HEAD (headers only), and the less common PUT, DELETE, PATCH, OPTIONS. Normal WordPress browsing uses GET, POST, and HEAD.
It’s tempting to block “the dangerous ones,” but here’s the trap: blocking methods indiscriminately breaks modern WordPress. The REST API, WooCommerce, webhooks, and many integrations use methods beyond GET/POST — the REST API in particular uses PUT, DELETE, and PATCH for legitimate operations, and OPTIONS for CORS preflight requests. Block those and you break the block editor, WooCommerce operations, and any API-driven feature.
So rather than an aggressive universal block, the guidance is: understand what your site uses before restricting anything. A pure brochure site with no REST/API usage might safely restrict to GET/POST/HEAD — but even then, test the block editor thoroughly. A WooCommerce or headless or integrated site should generally leave methods alone and handle abuse at the WAF layer, which understands context far better than a blunt method block.
Warning: A “block all methods except GET and POST” rule is a classic way to mysteriously break the block editor and WooCommerce, because it kills the REST API’s PUT/DELETE/PATCH/OPTIONS. If you’re not certain your site never needs those, don’t block them here.
Block suspicious user agents
What it is. User-Agent filtering blocks requests whose User-Agent header matches known-bad bots, scrapers, or scanners.
# Illustrative example — block a couple of bad bots by user agent
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_USER_AGENT} (badscraperbot|evilscanner) [NC]
RewriteRule .* - [F,L]
</IfModule>
Why it’s weak. User-Agent strings are trivially spoofed. An attacker sets their User-Agent to whatever they like — including a legitimate browser’s — in one line of code. So while this filters out lazy, honest-about-themselves bad bots (which do exist and do generate noise), it provides essentially no protection against a real attacker, who will simply not announce themselves.
When to use it: as minor noise reduction against known-bad automated traffic, and only as an illustrative, occasionally-updated list — never as a security control you rely on. Maintaining a big permanent blacklist is low-value busywork.
The risk: you can accidentally block legitimate crawlers or tools if your patterns are too broad. Be conservative.
How to test: confirm normal browsers and legitimate crawlers (Googlebot, etc.) still work.
How to undo it: remove the rewrite condition.
Security Note: Treat User-Agent blocking as cosmetic. Real bot protection comes from behavioral analysis at a WAF or CDN, which looks at how a client behaves rather than what it claims to be.
Block suspicious query strings
Attackers probe sites with malicious-looking query parameters — SQL-injection fragments, XSS payloads, path-traversal patterns (../), and scanner signatures. You can pattern-match some of these in .htaccess:
# Conservative example — block obvious traversal and script tags in query strings
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{QUERY_STRING} (\.\./|\.\.%2f) [NC,OR]
RewriteCond %{QUERY_STRING} (<script|%3Cscript) [NC]
RewriteRule .* - [F,L]
</IfModule>
Why to be cautious. Query-string filtering with regex is a false-positive minefield. Legitimate requests — search queries, plugin parameters, encoded data, even some page content — can contain patterns that look malicious to a naive regex. Too-aggressive rules block real users and break features; too-loose rules catch nothing meaningful. Getting it right for real-world attack traffic is genuinely hard.
The honest recommendation: keep any .htaccess query-string rules conservative and narrow (like the traversal example above), and lean on a WAF for real attack detection. A WAF is purpose-built for this — it maintains tuned rulesets, understands context, and updates as new attack patterns emerge. Trying to reproduce that in .htaccess regex is the wrong tool for the job.
How to test: thoroughly — search, filtered archives, plugin features, WooCommerce. Watch for legitimate requests getting 403.
How to undo it: remove the rewrite conditions.
Prevent hotlinking
What it does: stops other websites from embedding your images directly, using your bandwidth.
# Prevent hotlinking (adjust domain; allow blanks and search engines carefully)
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https?://(www\.)?example\.com [NC]
RewriteRule \.(jpg|jpeg|png|gif|webp)$ - [F,L]
</IfModule>
Why it matters. Hotlinking is bandwidth theft — another site displays your images while your server pays for the traffic. Hotlink protection blocks image requests whose referrer isn’t your own domain (the !^$ allows blank referrers, which matter for direct access and some legitimate cases).
This is more a resource/cost concern than a core security control, but it’s a common .htaccess use, so it’s worth including with its caveats.
The risks / common problems:
- CDNs — if you serve images via a CDN, the referrer logic needs to account for it.
- Subdomains —
img.example.comwon’t matchexample.comwithout adjustment. - REST API and social sharing — social platforms and link previews fetch images; overly strict rules break your Open Graph previews.
- Search engines — Google Images and others need access; block them and you lose image search traffic.
- Image previews — various legitimate services fetch images and can be caught.
How to test: confirm your own pages show images, social share previews render, and (if used) your CDN still serves images.
How to undo it: remove the rewrite block.
Add security headers through .htaccess
Security headers instruct the browser to enforce protections. You can set them in .htaccess (requires Apache’s mod_headers). Let’s go through the useful ones — with honest warnings, because one of these can break your whole site.
<IfModule mod_headers.c>
# Prevent MIME-type sniffing
Header set X-Content-Type-Options "nosniff"
# Control referrer information
Header set Referrer-Policy "strict-origin-when-cross-origin"
# Restrict powerful browser features you don't use
Header set Permissions-Policy "geolocation=(), microphone=(), camera=()"
# Force HTTPS for future visits (see HSTS warning below)
# Header set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</IfModule>
X-Content-Type-Options: nosniff — stops browsers from second-guessing declared content types (a defense against certain content-confusion attacks). Low risk; safe to add.
Referrer-Policy — controls how much referrer info your site leaks to other sites. strict-origin-when-cross-origin is a sensible default. Low risk.
Permissions-Policy — disables browser features (camera, microphone, geolocation) your site doesn’t use, shrinking abuse surface. Only disable what you genuinely don’t need.
Strict-Transport-Security (HSTS) — tells browsers to always use HTTPS for your domain going forward.
Warning: Only enable HSTS once HTTPS is fully working across your whole site and subdomains. It’s sticky — browsers remember it for the
max-ageduration (a year in the example), and if HTTPS later breaks, visitors can’t fall back to HTTP. Start with a shortmax-ageto test, and be cautious withincludeSubDomains.
Content-Security-Policy (CSP) — the most powerful and the most dangerous.
Warning: CSP controls which sources of scripts, styles, fonts, and other resources are allowed. A misconfigured CSP will silently break your site — Gutenberg, Elementor and other page builders, Google Analytics, ad scripts, embedded fonts, and countless plugins can all stop working with no obvious error. Do not paste a copied CSP. If you implement one, build it gradually using Content-Security-Policy-Report-Only mode first (which reports violations without enforcing), watch what it would block, allowlist your legitimate sources, and only then enforce. CSP on a complex WordPress site is a project, not a one-liner.
How to test: use your browser’s dev tools (Network → Headers) to confirm headers are present, and — especially after any CSP change — click through the whole site and editor to catch silent breakage. Online header scanners can verify the response too.
How to undo it: remove the relevant Header set lines.
Security Note: I deliberately didn’t hand you a giant copy-paste header block. The safe ones above are fine to add; CSP and HSTS need deliberate, tested rollouts. A header that breaks your checkout helps no one.
Force HTTPS carefully
What it does: redirects HTTP requests to HTTPS.
# Force HTTPS (test carefully behind proxies/CDNs)
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</IfModule>
Why it matters. Every page should be served over HTTPS. This redirect ensures anyone arriving via http:// gets bounced to https://.
The big risk — redirect loops. Here’s the subtle part: if your site sits behind Cloudflare, a load balancer, or a reverse proxy that terminates SSL, then by the time the request reaches Apache it may already be plain HTTP internally (the proxy handled the encryption). In that setup, %{HTTPS} off is always true, so Apache redirects to HTTPS → the proxy forwards as HTTP again → Apache redirects again → infinite loop, and your site dies with a “too many redirects” error.
The fix in proxied setups is to check the X-Forwarded-Proto header the proxy sets instead:
# For sites behind a proxy/CDN that sets X-Forwarded-Proto
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</IfModule>
When to use which: the first (%{HTTPS} off) for direct Apache with SSL on the server; the second (X-Forwarded-Proto) when a proxy/CDN terminates SSL. Using the wrong one is the classic redirect-loop cause.
Pro Tip: Many hosts and most CDNs (including Cloudflare) can force HTTPS at their level, which avoids the
.htaccessloop risk entirely. If yours does, that’s often the cleaner place to handle it. Also — WordPress’s own Site Address/WordPress Address (https://) plus a host/CDN HTTPS setting frequently covers this without touching.htaccessat all.
How to test: visit http://example.com and confirm a single clean 301 to https:// (not a loop). Check a few inner pages too.
How to undo it: remove the redirect block.
Block access to sensitive WordPress paths
Some files exist mainly to reveal information: readme.html (shows your WordPress version), license.txt, leftover debug.log, and various backup/temp files. You can restrict them:
# Restrict informational and leftover files
<FilesMatch "^(readme\.html|license\.txt|debug\.log|wp-config\.php\.bak)$">
Require all denied
</FilesMatch>
Why it’s minor. Hiding readme.html removes one easy way to read your exact WordPress version — but this is defense-in-depth, not real security.
Security Note: Hiding your WordPress version does not make your site secure. An unpatched site is exploitable whether or not the version is visible; a patched site is fine even if it is. Version-hiding slightly slows down lazy automated scanning and nothing more. Patch promptly — don’t rely on obscurity. (The
debug.logentry above matters more: a public debug log can leak paths and queries, so blocking or relocating it is genuinely worthwhile.)
How to test: request each file and confirm 403.
How to undo it: remove the entries.
Prevent unauthorized file access with <Files>
Apache’s <Files> and <FilesMatch> directives are the precise tools for protecting specific files — and precision is exactly the point. <Files "exact-name.php"> targets one file; <FilesMatch "pattern"> targets a pattern.
The principle worth repeating: explicit, named protection is safer than broad rules that catch legitimate assets. A rule that denies one clearly-sensitive file will never surprise you. A sweeping rule that denies “everything matching some broad pattern” eventually blocks something you needed — a plugin asset, a legitimate script — and you spend an afternoon figuring out why a feature broke.
So when you protect files, prefer the surgical approach: name the specific files or use tight, well-understood patterns, and test that nothing legitimate got caught in the net.
Rate limiting: what .htaccess can and can’t do
Let’s set expectations honestly: .htaccess is not a rate-limiting system. It can deny requests based on rules, but it has no native, robust way to say “block this IP after 10 login attempts in a minute.”
Real protection against brute-force attacks, bot floods, and login abuse comes from purpose-built layers:
- A WAF — filters and rate-limits malicious traffic with tuned, context-aware rules.
- A CDN — absorbs and throttles floods at the edge, before they reach your server.
- Fail2ban (server-level) — watches logs and dynamically bans IPs that trip thresholds.
- Server-level rate limiting — Apache modules like
mod_evasive, or Nginx’slimit_req, operate at the server tier with real counters.
Security Note: Trying to build a full firewall entirely in
.htaccessis the wrong approach. You’ll end up with a brittle, hard-to-maintain pile of rules that still doesn’t do proper rate limiting and risks breaking your site. Use.htaccessfor the file/access hardening it’s genuinely good at, and use a WAF/CDN/Fail2ban for traffic filtering and rate limiting. Right tool, right job.
Common .htaccess security mistakes
The greatest hits, each with its real-world consequence:
- Editing without a backup — one typo and you’re locked out with no clean file to restore.
- Removing the WordPress rewrite rules — permalinks break; inner pages 404 sitewide.
- Using rules copied from random sites — you inherit their assumptions, their Apache version, and their bugs.
- Blocking all POST requests — kills every form, comment, login, and checkout on the site.
- Blocking all query strings — breaks search, filtered archives, and countless plugin features.
- Blocking all PHP files — breaks WordPress entirely (it is PHP).
- Disabling XML-RPC without checking integrations — silently breaks Jetpack, the mobile app, or a remote-publishing workflow.
- Blocking REST API requests — breaks the block editor, WooCommerce operations, and integrations.
- Using outdated Apache 2.2 syntax on Apache 2.4 —
Order/Allow/Denymay fault → instant 500. - Mixing 2.2 and 2.4 authorization syntax — unpredictable behavior and errors.
- Creating redirect loops — usually a bad HTTPS rule behind a proxy; “too many redirects,” site unreachable.
- Adding duplicate rewrite rules — conflicting rules cause erratic routing.
- Overly broad regex — false positives block legitimate users and requests.
- Accidentally blocking search engine crawlers — a too-broad user-agent or method rule tanks your SEO.
- Forgetting CDN/proxy behavior — IP and HTTPS rules misfire because Apache sees the proxy, not the user.
- Storing backups in public directories — the archive rule is a net; the real fix is not putting them there.
- Assuming
.htaccessprotects against malware — it doesn’t scan or remove anything. - Assuming
.htaccessreplaces a WAF — different tool, different capabilities. - Making many changes at once — when something breaks, you can’t tell which rule did it.
- Not testing WooCommerce after changes — cart, checkout, and payment flows are exactly what aggressive rules break, and you find out from lost sales.
Apache 2.2 vs Apache 2.4 syntax
This trips up more people than almost anything else, because so many .htaccess snippets online are old.
Apache 2.2 (legacy) authorization:
Order allow,deny
Deny from all
# or
Order allow,deny
Allow from all
Apache 2.4+ (modern) authorization:
Require all denied
# or
Require all granted
Why it matters: on Apache 2.4 (what most modern hosting runs), the old Order/Allow/Deny directives require the mod_access_compat module to work at all. If that module isn’t loaded, those directives throw an error and cause a 500. So a “protect wp-config.php” snippet you copied from a 2013 blog post using Deny from all can take your whole site down on a modern server.
The safe move: use 2.4 syntax (Require all denied / Require all granted / Require ip …) on modern hosting, and don’t mix the two styles in the same file. If a copied snippet uses Order/Allow/Deny, translate it before using it.
| Goal | Apache 2.2 (legacy) | Apache 2.4+ (modern) |
|---|---|---|
| Deny everyone | Order allow,deny + Deny from all | Require all denied |
| Allow everyone | Order allow,deny + Allow from all | Require all granted |
| Allow one IP | Order deny,allow + Deny from all + Allow from IP | Require ip IP |
.htaccess security for WooCommerce
WooCommerce deserves its own warning, because it’s where aggressive .htaccess rules do the most expensive damage — broken checkout means lost revenue, not just a broken widget.
WooCommerce leans heavily on dynamic requests, and the rules in this guide that most often break it:
- Cart and checkout — depend on POST requests and sessions; method-blocking rules break them.
- My Account — customer flows that a blanket
wp-adminauth or aggressive rule can disrupt. - AJAX (
admin-ajax.php) — used constantly for cart updates; broken by over-broadwp-admin/method rules. - REST API — WooCommerce uses it for many operations, including PUT/DELETE; method or REST blocking breaks it.
- Payment gateways and webhooks — external services POST to your site; block those methods or filter those requests and payments silently fail.
- Product images — hotlink or file rules that are too broad can break image display.
- Dynamic requests generally — query-string filtering can catch legitimate WooCommerce parameters.
Warning: After any
.htaccesschange on a WooCommerce site, test the full purchase flow end to end — add to cart, checkout, payment, order confirmation, and (if you can) a webhook callback. A rule that looks harmless can break payments in a way you won’t notice until orders stop arriving. When in doubt on a store, be conservative and lean on a WAF for request filtering instead of.htaccessregex.
.htaccess security for Elementor and page builders
Page builders like Elementor (and Divi, Beaver Builder, and friends) interact with a lot of moving parts: AJAX, the REST API, dynamically generated CSS, web fonts, JavaScript, and external services. That makes them unusually sensitive to over-aggressive .htaccess rules.
The symptoms of a conflict are maddening precisely because they’re not a clean 500 — they’re subtle frontend weirdness:
- Styling that partially loads or looks broken (dynamic CSS blocked)
- Fonts not rendering (font files or external font services blocked)
- The editor failing to save or load (AJAX/REST blocked)
- Features silently not working (method or query-string rules interfering)
If you harden a site running a page builder and then notice mysterious frontend problems, your recent .htaccess changes are the first suspect. Roll back to your known-good file, confirm the problem disappears, then re-add rules one at a time — testing the builder’s front end and editor after each — to find the culprit.
Pro Tip: Test page-builder sites in the editor as well as the front end after security changes. A rule can leave the public page looking fine while quietly breaking the editing experience, which you won’t catch by only viewing the live site.
Testing your .htaccess security rules
After every change, work through this. The goal is to catch breakage you introduced before your visitors do.
Test these surfaces:
- Homepage
- A blog post and a page
- wp-admin (dashboard loads)
- wp-login.php (you can log in)
- Media uploads (upload and display an image)
- Site search
- REST API (block editor works — it relies on REST)
- XML-RPC (only if you need it — Jetpack/app/remote publishing)
- Contact forms
- WooCommerce checkout (if applicable — full purchase flow)
- Payment gateway (if applicable)
- AJAX-driven features
- Elementor/Gutenberg editor
- Mobile view
- HTTPS (padlock, no mixed-content warnings)
- Redirects (no loops)
- CDN behavior (if applicable)
- Cache (purge and retest)
And learn to read the status codes you’ll see:
| Status | Meaning | In this context |
|---|---|---|
| 200 | OK | The page/resource loaded normally — good |
| 301 / 302 | Redirect | Expected for HTTPS/URL redirects; unexpected loops are bad |
| 403 | Forbidden | Your deny rule is working (good on protected files; bad on legitimate pages) |
| 404 | Not Found | Missing resource — or a broken rewrite if it’s sitewide |
| 500 | Server Error | An .htaccess (or server) error — roll back immediately |
Check the server error log. Your hosting panel exposes an Apache/error log, and it’s the fastest way to diagnose a 500 — it’ll usually name the offending directive or module. When something breaks and you’re not sure why, the error log is where the answer is.
Pro Tip:
curl -I https://example.com/pathshows you the status code and headers for any URL without a browser — perfect for quickly confirming a403on a file you meant to protect, or a clean301on a redirect, without clicking around.
Troubleshooting HTTP 500 after editing .htaccess
If you saved a change and the site 500’d, don’t panic — this is almost always fast to fix. Work these steps in order.
Step 1 — Get filesystem access. Open your hosting File Manager or connect via SFTP. This works even with the site down, because it doesn’t depend on WordPress.
Step 2 — Rename .htaccess. Rename it to something like .htaccess-broken. This effectively disables it.
Step 3 — Test the site. Refresh. If it loads now, you’ve confirmed .htaccess was the cause. (If it still 500s, the problem is elsewhere — PHP, a plugin, file permissions — and you can rename .htaccess back.)
Step 4 — Restore your known-good file. Put your backup copy in place as .htaccess. The site should be fully working again. Crisis over.
Step 5 — Re-add rules one at a time. Now reintroduce your intended changes individually, testing after each. When the 500 returns, the last rule you added is the culprit.
Step 6 — Check the server error log. For the offending rule, the Apache error log usually tells you exactly what’s wrong — an unknown directive, a missing module, a syntax error.
A 500 after an .htaccess edit almost always means one of: invalid or unsupported directives (e.g., 2.2 syntax on 2.4 without the compat module), a required Apache module not loaded (like mod_headers or mod_rewrite), or a plain syntax error (a typo, an unclosed <Files> block). The rename-and-restore dance above resolves the emergency; the error log explains the cause.
A practical WordPress .htaccess security baseline
Here’s a conservative baseline — not a 100-rule monster, just broadly-appropriate rules with the optional ones clearly marked. Add the core section first, test, then consider the rest based on your site.
Placement: put custom rules outside the # BEGIN WordPress / # END WordPress block (above it is fine).
Core hardening (generally useful for most Apache/LiteSpeed sites)
# Disable directory browsing
Options -Indexes
# Protect wp-config.php (Apache 2.4+)
<Files "wp-config.php">
Require all denied
</Files>
# Protect .htaccess / .htpasswd
<Files ~ "^\.ht">
Require all denied
</Files>
# Block backup and archive files
<FilesMatch "\.(bak|old|save|swp|orig|sql|zip|tar|gz|tgz)$">
Require all denied
</FilesMatch>
# Safe security headers
<IfModule mod_headers.c>
Header set X-Content-Type-Options "nosniff"
Header set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
Plus a separate file at wp-content/uploads/.htaccess:
# wp-content/uploads/.htaccess — no PHP execution here
<Files "*.php">
Require all denied
</Files>
Optional hardening (depends on your site)
# Block xmlrpc.php — ONLY if nothing you use needs it
<Files "xmlrpc.php">
Require all denied
</Files>
# Restrict informational/leftover files
<FilesMatch "^(readme\.html|license\.txt|debug\.log)$">
Require all denied
</FilesMatch>
# Permissions-Policy — disable features you don't use
<IfModule mod_headers.c>
Header set Permissions-Policy "geolocation=(), microphone=(), camera=()"
</IfModule>
Advanced hardening (experienced admins, test thoroughly)
# HSTS — ONLY after HTTPS is fully working everywhere (sticky!)
# <IfModule mod_headers.c>
# Header set Strict-Transport-Security "max-age=31536000; includeSubDomains"
# </IfModule>
# Content-Security-Policy — build with Report-Only first; can break the site
# (Not included here on purpose — implement deliberately, not by copy-paste.)
Site-specific rules (add per requirements)
- wp-login.php IP restriction — only with a static/VPN IP and awareness of CDN/proxy behavior.
- HTTPS redirect — use the correct variant for direct-SSL vs proxy/CDN, or handle it at the host/CDN level.
- HTTP auth on
/wp-admin/— dev/staging/internal sites only; watch foradmin-ajax.phpbreakage on public sites.
Notice what’s not here: no blanket method blocking, no aggressive query-string regex, no giant user-agent blacklist, no CSP copy-paste. Those are either better handled by a WAF or too risky for a general baseline. Restraint is a feature.
WordPress .htaccess security checklist (30+ points)
Save this and work through it deliberately.
Preparation
- Confirm your server is Apache or LiteSpeed (not Nginx)
- Confirm File Manager / SFTP access works
- Back up the current
.htaccess - Know your rollback procedure
Core hardening
- Disable directory browsing (
Options -Indexes) - Protect
wp-config.php - Protect
.htaccess/.ht*files - Block backup/archive file extensions
- Prevent PHP execution in
wp-content/uploads/ - Confirm you’re using Apache 2.4 syntax
- Keep custom rules outside the
# BEGIN/END WordPressblock
File & path protection
- Remove any actual backups from public directories
- Protect/relocate
debug.log - Restrict informational files (readme, license) — as defense-in-depth only
- Protect other sensitive files deliberately (not with blanket rules)
Optional / conditional
- Review whether XML-RPC is needed before blocking
- Review admin access strategy (login page vs wp-admin vs HTTP auth)
- Decide HTTPS handling (host/CDN vs
.htaccess, correct proxy variant)
Security headers
- [ ] Add
X-Content-Type-Options: nosniff - [ ] Add a sensible
Referrer-Policy - [ ] Add
Permissions-Policyfor unused features - [ ] Plan HSTS carefully (only after full HTTPS)
- [ ] Treat CSP as a deliberate project (Report-Only first)
Testing after every change
- Homepage, posts, pages load (200)
- wp-admin and login work
- Media upload/display works
- Site search works
- REST API / block editor works
- XML-RPC works (if needed)
- Contact forms submit
- WooCommerce cart/checkout/payment (if applicable)
- AJAX features work
- Page builder front end and editor work
- HTTPS clean, no redirect loops, no mixed content
- CDN/cache purged and retested
- Server error log checked for warnings
Ongoing
- Test after every future rule change
- Re-verify after WordPress/plugin/host changes
- Remember
.htaccessis one layer, not your whole security
What .htaccess cannot protect you from
Time for the honest boundary. .htaccess hardening is real and worthwhile, but it does not replace any of these:
- WordPress core updates —
.htaccesscan’t patch a core vulnerability. - Plugin and theme updates — the biggest real-world attack surface; unaffected by
.htaccess. - Strong passwords and MFA —
.htaccessdoesn’t manage authentication strength. - Least-privilege accounts — a user/role concern, not a server-rule one.
- Secure hosting and server patching — the layer beneath
.htaccess. - Malware scanning and removal —
.htaccessneither detects nor cleans malicious code. - Backups — no rule recovers a lost or ransomed site.
- A WAF and CDN protection — real request filtering and DDoS mitigation live here.
- Database security — credentials, privileges, and access are outside
.htaccess. - Vulnerability management and file-integrity monitoring — ongoing processes, not static rules.
Security Note: Understand the difference between hardening and complete security. Hardening reduces specific attack surfaces — that’s what
.htaccessdoes well. Complete security is the combination of many layers, each covering what the others can’t..htaccessis a good layer. It is not the wall.
The WordPress security stack
Here’s where .htaccess sits in a defense-in-depth model. Each layer catches what the ones above it let through:
Internet
↓
CDN / DDoS Protection ← absorbs floods at the edge
↓
WAF ← filters malicious requests
↓
Web Server / Apache ← serves the site
↓
.htaccess Hardening ← blocks sensitive files, stops PHP in uploads ← YOU ARE HERE
↓
PHP ← runs the code
↓
WordPress Core ← kept updated
↓
Themes + Plugins ← vetted and updated
↓
Database ← least privilege, strong credentials
↓
Backups + Monitoring ← recovery and detection
Read top to bottom: .htaccess is one specific, valuable band in the middle. It does a job the WAF and CDN don’t (server-level file and access control) and can’t do the jobs above and below it. Remove any single layer and you’ve created a gap the others weren’t designed to fully cover. That’s the whole philosophy — defense in depth, not one hero control.
.htaccess vs security plugin vs WAF
These three are complementary, not competing. Here’s how they compare:
| Feature | .htaccess | Security Plugin | WAF |
|---|---|---|---|
| File protection | Yes | Sometimes | No/limited |
| Login protection | Limited | Yes | Yes |
| Malware scanning | No | Yes | No |
| Request filtering | Limited | Yes | Yes |
| Rate limiting | Limited | Yes | Yes |
| Bot protection | Limited | Yes | Yes |
| Server-level rules | Yes | No | No |
| Performance impact | Low/varies | Medium/varies | Usually low at origin |
| Best use | Server hardening | WordPress-level security | Network/request filtering |
How to think about it:
.htaccessis your server-level hardening — protecting files, stopping PHP execution in uploads, setting headers. Things that happen before WordPress loads.- A security plugin operates inside WordPress — malware scanning, login protection, file-integrity monitoring, activity logs. Things that need WordPress context.
- A WAF operates at the network/request layer — filtering malicious traffic, rate limiting, bot mitigation, virtual patching. Things best done before traffic reaches your origin.
