# Block PHP Script Scans Without Blocking Visitors: a 404 Rule and fail2ban (Tested)

Source: https://srvscripts.com/guides/block-php-script-scans-fail2ban/
Updated: 2026-10-18
Publisher: srvScripts (https://srvscripts.com/)

**Short answer:** to handle PHP script scans, stop missing .php requests from starting PHP with a short rewrite rule, then let fail2ban ban addresses that request many missing .php files in a short time. On our test server a WordPress-style site slowed from 32 ms to 346 ms per page under a scan flood; with the rule it stayed at 40 ms, and fail2ban banned the scanning address after its 20th request. Do not block whole cloud networks: that also blocks legitimate services.

In short: Stop missing .php requests from starting PHP with a short rewrite rule, then let fail2ban ban addresses that request many missing .php files in a short time. On our test server a WordPress-s…

Every public server sees scanners that request hundreds of PHP files that do not exist: `wp-config.bak.php`, `shell.php`, `.env.php` and so on, often from rotating addresses on cloud networks. A [recent DirectAdmin forum thread](https://forum.directadmin.com/threads/how-to-properly-deal-with-php-script-attacks.82709/) asks exactly this: how to cut the CPU load without blocking real visitors. The scans themselves are mostly harmless. What hurts is what each request costs, and on a typical WordPress or forum site that cost is a full run of the application.

## What PHP script scans cost: our measurement

We sent 20,000 requests for a missing PHP file to four Apache setups on the same 4-vCPU Debian 12 server (Apache 2.4.68, PHP-FPM 8.2, five PHP workers), 50 at a time, and measured how many reached PHP and how fast a real page loaded meanwhile.

| Setup | Scan requests that reached PHP | Real page during the flood |
| --- | --- | --- |
| PHP-FPM handler without a file check | all 20,000 | not measured separately (no application) |
| PHP-FPM handler with a file check (as in DirectAdmin’s templates) | none | — |
| WordPress-style front controller (missing files are rewritten to index.php, about 30 ms of PHP work each) | every request; all 5 PHP workers busy | 346 ms on average (32 ms before), up to 402 ms |
| The same with the 404 rule from step 1 | none | 40 ms on average |

The third row is the important one. A file check in the PHP handler does not help a WordPress, phpBB or similar site, because their `.htaccess` rules send every request for a missing file to `index.php`, so each scan starts the application. We repeated the check with a real WordPress 7.1.3 site on OpenLiteSpeed 1.9.3: a missing `.php` URL produced the WordPress “Page not found” page in 51 to 70 ms, and each one was a separate LiteSpeed Cache miss. With the rule, the same requests took 0.7 to 1.3 ms.

## Step 1: answer missing .php files before PHP starts

Put these lines at the top of the site’s `.htaccess`, above the WordPress (or forum) rules:

```
# Missing .php files get a plain 404 from the web server before WordPress starts
RewriteEngine On
RewriteCond %{REQUEST_URI} \.php$ [NC]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ - [R=404,L]
```

- Real PHP files such as `wp-login.php` and `index.php` still work, and pretty URLs (`/about-us/`) still go to WordPress. We checked all three after adding the rule.

- Before you add it, look for plugins or apps that answer URLs ending in `.php` that are not real files. Search the access log for `.php` requests that returned 200, and check each one exists on disk.

- Apache reads `.htaccess` on every request. OpenLiteSpeed reads rewrite rules from `.htaccess` too, but only picks up changes after a graceful restart (`/usr/local/lsws/bin/lswsctrl restart`). We tested OpenLiteSpeed with the same rules in the virtual host’s rewrite section.

- On DirectAdmin with Apache and PHP-FPM, the templates in `/usr/local/directadmin/data/templates/virtual_host2.conf` already wrap the PHP handler in `<If "-f %{REQUEST_FILENAME}">` (checked on DirectAdmin 1.712), so only the application’s rewrite rules send scans to PHP. Custom templates should keep that check.

## Step 2: ban addresses that keep requesting missing .php files

A rule makes each scan cheap; fail2ban stops a single address from sending thousands of them. Create the filter `/etc/fail2ban/filter.d/php-404.conf`:

```
[Definition]
failregex = ^ -.*"(?:GET|POST|HEAD) [^" ]*\.php(?:\?[^" ]*)? HTTP/[0-9.]+" (?:403|404)
ignoreregex =
```

It matches a `.php` request answered with 404 or 403, with or without a query string, and ignores everything else. Test it against your own log before you enable it:

```
fail2ban-regex /var/log/apache2/access.log /etc/fail2ban/filter.d/php-404.conf
```

On our fixture of nine typical lines it matched the four scan requests (`wp-config.bak.php`, `xmlrpc.php` refused with 403, `shell.php?cmd=id`, `.env.php`) and none of the five normal ones (`index.php` with 200, a redirect, a missing image, a missing page). Then add the jail, `/etc/fail2ban/jail.d/php-404.local`:

```
[php-404]
enabled  = true
port     = http,https
filter   = php-404
logpath  = /var/log/apache2/access.log
maxretry = 20
findtime = 60
bantime  = 3600
action   = iptables-multiport[name=php-404, port="http,https", protocol=tcp]
```

- **20 in 60 seconds** is far above what a person or a search engine does; adjust it to your logs. Add your own and your monitoring addresses to `ignoreip`.

- The explicit `action` line matters on systems whose `jail.local` does not define default actions; on our FreePBX test server the jail loaded without it, with no action at all, so it would have found scans and banned nobody. Use `nftables-multiport` instead if your fail2ban uses nftables.

- On DirectAdmin, the per-domain Apache logs are `/var/log/httpd/domains/DOMAIN.log`, so use `logpath = /var/log/httpd/domains/*.log`. The error logs in the same folder also match the pattern; they never match the filter, so this only costs a little reading.

- Reload with `fail2ban-client reload`, check with `fail2ban-client status php-404`. If the jail shows no actions, restart fail2ban.

**Our result:** 160 normal requests from one address (home page, admin page, 30 missing images) in 11 seconds were not counted at all. Then the same address requested `/scan-1.php`, `/scan-2.php` and so on: requests 1 to 20 got a 404, fail2ban banned the address 1.5 seconds after the first one, and request 21 was refused. SSH on port 22 stayed reachable, because the ban covers ports 80 and 443 only. `fail2ban-client set php-404 unbanip ADDRESS` lifted it at once.

## If you use CSF instead of fail2ban

CSF has its own option, `LF_APACHE_404` in `/etc/csf/csf.conf` (csf v15.12 on our DirectAdmin server). It counts “File does not exist” lines in the Apache error log, and its own comment warns that it can produce many false positives and suggests a high value such as 200. It also cannot see the requests that matter most here: when we sent missing `.php` requests to the WordPress-style site, with or without the rule from step 1, the error log got no line at all, because the request was rewritten instead of failing. A fail2ban jail on the access log sees them. If CSF manages your firewall, a restart of CSF can clear rules other tools added, so check `fail2ban-client status` after `csf -r`. We did not test fail2ban and CSF together.

## What not to do

- **Block whole cloud networks** (Google Cloud, Azure). It cuts the scans but also blocks legitimate services, including mail servers and monitoring, and the lists change.

- **Rely on a rate limit per address alone.** Scanners rotate addresses; the rule from step 1 makes every request cheap whatever its source.

- **Return 403 for every `.php` file.** It breaks `wp-login.php`, `wp-cron.php` and admin pages. Only missing files should get the short answer.

## How we tested

On 18 October 2026, on our Debian 12 lab server (4 vCPU, Apache 2.4.68, PHP-FPM 8.2.34 with 5 workers, fail2ban 1.0.2 with iptables-nft): four test sites on local ports for the cost comparison, with `ab` sending 20,000 requests 50 at a time (or 15 seconds of continuous requests for the front-controller pair), and real page timings taken with `curl` during the flood. The front-controller site used a PHP script doing about 30 ms of work per request in place of WordPress. The fail2ban test used a second server of ours as the scanner. The WordPress check ran WordPress 7.1.3 with LiteSpeed Cache on OpenLiteSpeed 1.9.3 in a test container. The DirectAdmin template and CSF settings were read on our DirectAdmin 1.712 server; nothing was changed there.

Not tested: LiteSpeed Enterprise, nginx, cPanel’s log paths, and fail2ban combined with CSF. Numbers from one 4-vCPU server are a comparison, not a benchmark for your hardware.

Related: [block AI crawlers without leaving AI search](/guides/block-ai-crawlers/), [access log analyzer](/tools/access-log-analyzer/), [top IPs in web logs](/scripts/web-log-top-ips/).

## Frequently asked questions

### Will this block Googlebot or real visitors?

Not in our test: normal browsing, a missing image and a missing page are not counted. Search engines rarely request missing `.php` files twenty times in a minute. To be safe, add your monitoring and office addresses to `ignoreip`.

### Does this stop the scans?

No. Scanners keep trying, often from new addresses. The rule makes each attempt cost almost nothing, and fail2ban stops any single address that keeps going. Together they remove the CPU load, which is the actual problem.

### Should xmlrpc.php be blocked too?

If nothing on the site uses XML-RPC (the Jetpack plugin and some mobile apps do), refusing it with 403 is common. The filter above counts 403 answers too, so repeated attempts get the address banned.
