Guide · .htaccess malware

A few lines in .htaccess can redirect your visitors, hide a backdoor, or lock you out.

You opened .htaccess and found lines above the WordPress block that nobody on the team wrote: RewriteCond conditions on the referrer or the browser, a redirect to a domain you have never heard of, a php_value line, a handler that treats .ico files as PHP. Or every folder on the site suddenly has its own identical .htaccess. .htaccess is read by Apache on every request before WordPress runs, which makes it a favourite place to cloak redirects and protect a backdoor. This guide explains each kind of malicious rule, how to compare your files with WordPress’s standard block, and how to clean all of them without losing the evidence.

Symptom
Redirects only some visitors see, 403 errors on your own files, or odd lines in .htaccess
Mechanism
Apache rewrite, handler and PHP directives read before WordPress
Scope
The folder it sits in and every folder beneath it
Server type
Apache and LiteSpeed read .htaccess; Nginx ignores it

Why .htaccess

Apache reads it before WordPress, on every request

.htaccess is a per-folder configuration file for Apache and for LiteSpeed, which reads the same syntax. The server applies it before handing the request to PHP, so a rule in it decides what happens to a visitor before WordPress — and every WordPress security plugin — has been loaded. Anyone who can write a file in the site’s folders can change it; no server access or control-panel login is needed.

WordPress itself writes one block into the root .htaccess, between # BEGIN WordPress and # END WordPress, and rewrites that block when permalinks are saved. Everything outside those markers is somebody else’s: another plugin’s documented block (caching and security plugins add their own, also with BEGIN and END markers), your host’s, your developer’s — or an attacker’s. Malicious rules are usually placed above the WordPress block, so they run first.

If your site runs on Nginx only, .htaccess files are ignored and none of this applies — but the files are still worth reading, because their presence tells you what the attacker expected, and many hosts put Apache or LiteSpeed behind an Nginx front end.

The baseline

What WordPress’s own block looks like

This is the block current WordPress releases write for a site installed at the root of its domain. Sites installed in a subfolder have that folder in RewriteBase and the last RewriteRule; older installations may lack the three comment lines and the Authorization line.

Standard WordPress .htaccess block
# BEGIN WordPress
# The directives (lines) between "BEGIN WordPress" and "END WordPress" are
# dynamically generated, and should only be modified via WordPress filters.
# Any changes to the directives between these markers will be overwritten.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

# END WordPress

Anything between those markers that differs from this, and anything outside them that no plugin, host or developer can account for, needs explaining. Compare the root file first, then every other .htaccess in the account.

Figure 1 · Same file, two purposes

WordPress’s block sends requests to index.php. The added rules decide who gets sent elsewhere.

Standard WordPress 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

Only routes requests to index.php.

Added above it (investigate)
RewriteEngine On
RewriteCond %{HTTP_REFERER} (google|bing|yahoo) [NC]
RewriteCond %{HTTP_COOKIE} !wordpress_logged_in [NC]
RewriteRule ^(.*)$ hxxp://redirect.example/go?s=$1 [R=302,L]

Search visitors who are not logged in go elsewhere; the owner never does.

The standard block (its three explanatory comments and the Authorization line omitted for space) routes every request that is not a real file or folder to index.php. The rules on the right send search visitors who are not logged in to another site, so the owner never sees the redirect. The domain is illustrative and defanged. Illustrative reconstruction.

What the rules do

Four kinds of malicious .htaccess rule

Cloaked redirects

RewriteCond lines test who is asking — a search engine in HTTP_REFERER, a phone in HTTP_USER_AGENT, the absence of a wordpress_logged_in cookie — and a RewriteRule with R=301 or R=302 sends only those visitors to another domain. The owner, logged in on a desktop, never sees it.

Example · redirect for search visitors only
RewriteCond %{HTTP_REFERER} (google|bing|yahoo) [NC]
RewriteCond %{HTTP_COOKIE} !wordpress_logged_in [NC]
RewriteRule ^(.*)$ hxxp://redirect.example/go?s=$1 [R=302,L]

Variants test for crawlers instead, serving spam pages only to search engines.

A prepended loader

Under mod_php, php_value auto_prepend_file in .htaccess makes PHP run a named file before every script, exactly like the .user.ini version. On PHP-FPM hosting Apache does not recognise php_value and the site returns 500 errors instead — which is itself a clue.

Example · a loader in uploads
php_value auto_prepend_file "/home/user/public_html/wp-content/uploads/.cache.php"

The directive is neutral; the file it names decides whether it is malicious.

Images and icons run as PHP

AddHandler, AddType, SetHandler or ForceType lines that hand .ico, .png, .jpg or .txt files to PHP. The attacker then hides a backdoor in a file named favicon_a3f9.ico that no one opens and no scanner treats as code.

Example · .ico handled as PHP
<FilesMatch "\.ico$">
SetHandler application/x-httpd-php
</FilesMatch>

The exact handler name differs between hosts and PHP versions; any handler line that gives a non-PHP extension to PHP is the finding.

Lockdown files

An identical .htaccess dropped into every folder that denies all PHP files except a short list of names — the attacker’s backdoors. It stops other attackers and some of your own tools, and keeps their files reachable. A lockdown file in wp-admin can also lock the owner out of the dashboard.

Example · deny everything except named files
<FilesMatch "\.(php|phtml|php5)$">
Order allow,deny
Deny from all
</FilesMatch>
<FilesMatch "^(index\.php|x7k\.php)$">
Order allow,deny
Allow from all
</FilesMatch>

Security plugins legitimately deny PHP in wp-content/uploads; the difference is the allow-list of specific file names.

Find them all

Search the whole account, read-only

Run these from the hosting account’s home directory over SSH, so folders above the site and neighbouring sites are included. None of them change anything.

  1. 01

    List every .htaccess with its size and date

    Recent dates in folders nobody has touched, and many files with the same size, are the first leads:

    shell
    find ~ -name '.htaccess' -ls 2>/dev/null
  2. 02

    Find files that are identical copies

    A lockdown file is usually the same file in dozens or hundreds of folders. Counting hashes shows it at once:

    shell
    find ~ -name '.htaccess' -exec sha256sum {} + 2>/dev/null | awk '{print $1}' | sort | uniq -c | sort -rn | head

    A hash that appears hundreds of times is worth reading; then list where it appears with grep on that hash.

  3. 03

    Find conditional redirects

    Every .htaccess that tests the referrer or the browser:

    shell
    grep -rlE --include='.htaccess' 'HTTP_REFERER|HTTP_USER_AGENT|HTTP_COOKIE' ~ 2>/dev/null

    Caching and security plugins test user agents too; read each match rather than deleting on sight.

  4. 04

    Find handler and PHP directives

    Lines that load a file before PHP scripts, or give other extensions to PHP:

    shell
    grep -rnE --include='.htaccess' 'auto_(prepend|append)_file|AddHandler|AddType|SetHandler|ForceType' ~ 2>/dev/null
  5. 05

    Check what a search visitor actually gets

    Request the home page as a phone arriving from Google, then as yourself, and compare the status and the Location header:

    shell
    curl -sI -A 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)' -e 'https://www.google.com/' https://example.com/ | grep -iE '^(HTTP|location)'
    curl -sI https://example.com/ | grep -iE '^(HTTP|location)'

    Replace example.com with your domain. A redirect in the first response and not the second is a cloaked rule — in .htaccess or elsewhere.

Fix it

Clean every file, then find what wrote them

  1. 01

    Preserve first

    Copy every suspicious .htaccess with its path and timestamps, and any file a rule points at, before editing. The timestamps bound the incident; the targets are often the real backdoors.

  2. 02

    Restore the root file to known content

    Replace the root .htaccess with WordPress’s standard block plus the blocks you can attribute to plugins, your host and your developer. Saving permalinks in Settings regenerates the WordPress block if mod_rewrite rules are writable.

  3. 03

    Remove the copies and the protected files

    Delete the lockdown files and handler rules everywhere they appear, then deal with the files they protected or pointed at — the allow-listed PHP names, the .ico files that were run as PHP, the prepended loader. Those are backdoors in their own right.

  4. 04

    Find the writer

    Rules that come back after you clean them are being rewritten: by a backdoor elsewhere in the account, a prepended loader, a scheduled task, or someone still logged in. Look for each, and rotate hosting, SFTP, database and WordPress credentials.

  5. 05

    Verify as a visitor, more than once

    Repeat the curl comparison and a search-visitor test from a phone over the following days. A cloaked redirect that returns means the cleanup missed a layer.

Common questions

Common questions about .htaccess malware

Is any rule outside the WordPress block malicious?

No. Caching, security, SEO and redirect plugins write their own blocks, usually between their own BEGIN and END markers, and hosts and developers add rules too. Anything you cannot attribute to one of those needs explaining, and anything that redirects only certain visitors to another domain is almost never legitimate.

Why do I not see the redirect when I visit my own site?

Because the rules test for it. A cloaked redirect typically fires only for visitors arriving from a search engine, on a phone, or without a logged-in cookie, and sometimes only once per visitor. Test as a logged-out phone visitor coming from a search result.

Every folder on my site has the same .htaccess. What is it?

Most likely a lockdown file: it denies all PHP except a few named files, which are the attacker’s. Read one, note the allowed names, find those files, and remove both, preserving the allowed files as evidence first.

I deleted the bad lines and they came back. Why?

Something with write access is restoring them: a backdoor elsewhere in the account, a prepended loader, a scheduled task, or stolen credentials. Fixing the file treats the symptom; finding the writer ends it.

My site is on Nginx. Do I need to check .htaccess?

Nginx does not read .htaccess, so the rules have no effect there. Many hosts run Apache or LiteSpeed behind Nginx, though, and the files still tell you what was planted and where, so read them before removing them.