Every PHP application I've worked on eventually faces the same problem: bots. Not sophisticated attackers — just automated scanners running 24/7, checking every server on the internet for `/wp-admin`, `/.env`, `phpinfo.php`, SQL injection vectors, and whatever CVE was published last week.

The usual answer is Cloudflare, or ModSecurity, or some hosted WAF service. Those are good solutions. But they assume you *can* use them. Sometimes you can't — a shared hosting environment, a client with compliance constraints, a legacy server where you have PHP access but not Apache module access. Sometimes you just want a protection layer that lives inside your application and requires nothing beyond PHP itself.

That's the specific problem xZeroProtect tries to solve.

What It Is (and What It Isn't)

xZeroProtect is a PHP 8 library that acts as an application-layer firewall. You drop it into your bootstrap file and it inspects every incoming request before your application logic runs.

It is not a replacement for a properly configured server, a CDN firewall, or network-level protection. It operates at the PHP layer, which means malicious requests still reach your server and consume a small amount of resources before being rejected. For volumetric DDoS attacks, you need something upstream.

What it is useful for: stopping the constant background noise of automated scanners, blocking known malicious tools by their signatures, catching basic injection attempts in request parameters, and rate-limiting abusive IPs — all without any database, Redis instance, or third-party service.

Why File-Based Storage?

The most common question about the architecture is why storage is file-based rather than MySQL or Redis.

The short answer is that a firewall library shouldn't require infrastructure that might not exist. If you're adding this to a project, you shouldn't need to create a database table or spin up a Redis container first. File-based storage using JSON files works on any hosting environment where PHP can write to disk.

The practical tradeoff is performance at scale. Storing rate-limit counters as JSON files is fine for most applications, but it's not the right choice if you're handling thousands of requests per second across multiple application servers. For those cases, a Redis-backed solution would be more appropriate. xZeroProtect is transparent about this — it's designed for the common case, not the extreme one.

How It Works

Initialization

The library is initialized once in your bootstrap file:

use Webrium\XZeroProtect\XZeroProtect;

XZeroProtect::init([
 'storage_path' => __DIR__ . '/storage/firewall',
 'mode' => 'production',
])->run();

`init()` stores the instance as a singleton internally, so you can retrieve it from anywhere in your application later:

$firewall = XZeroProtect::getInstance();

This means you don't need to thread a `$firewall` variable through your entire codebase to access logs or manage bans from an admin panel.

The Request Pipeline

When 'run()' is called, it processes the current request through a series of checks in order:

1. Whitelist check — IPs and paths in your whitelist bypass everything else. Supports CIDR notation for both IPv4 and IPv6.

2. Ban check — The request IP is checked against stored bans. Bans can be temporary (with a configurable duration) or permanent. Expired bans are cleaned up automatically.

3. Rate limiting — A sliding-window counter tracks requests per IP. The window size and maximum request count are configurable. The counter is stored as a JSON file per IP.

4. Path detection — The request URI is checked against a list of known suspicious paths. Out of the box this includes WordPress admin panels, `.env` files, database management tools, common web shell names, and several file extensions that shouldn't appear in a modern routed PHP application.

5. User-Agent detection — The `User-Agent` header is matched against known scanner and exploit tool signatures. Empty User-Agent strings are flagged as suspicious by default. The default list covers tools like sqlmap, nikto, dirbuster, masscan, and around thirty others.

6. Payload scanning — GET parameters, POST body, cookies, and raw input are scanned using PCRE regular expressions for attack patterns. The default ruleset covers SQL injection variants, XSS, path traversal, PHP code injection, LFI, RFI, and command injection.

7. Custom rules — Any rules you've registered run last. Custom rules receive the full request object and return a typed result: `pass`, `block`, or `log`.

Auto-Banning

When a request triggers a violation, the violation count for that IP is incremented in storage. Once the threshold is reached (default: 5 violations), the IP is automatically banned for a configurable duration. After a configurable number of temporary bans, the ban becomes permanent.

'auto_ban' => [
 'enabled' => true,
 'violations_threshold' => 5,
 'ban_duration' => 86400, // 24 hours
 'permanent_after_bans' => 3,
],

Apache Integration

For permanently banned IPs, there's an optional Apache integration that writes them to `.htaccess`:

# xZeroProtect:start
<RequireAll>
 Require all granted
 Require not ip 185.220.101.5
</RequireAll>
# xZeroProtect:end

This means Apache rejects those connections before PHP starts, which reduces the overhead for known bad actors. The `.htaccess` block is managed programmatically and won't conflict with other content in the file.

Extending the Defaults

One design decision worth explaining: the default rules try to be broadly useful but deliberately conservative. A `.php` extension in a URL is suspicious for a modern routed application, but might be completely normal for a legacy codebase. The defaults are a starting point, not a mandate.

Adding or removing patterns at runtime is straightforward:

// A fully-routed app with no .php URLs can add this:
$firewall->patterns->addPath('.php');

// If your API clients use curl, remove it from the agent blocklist:
$firewall->patterns->removeAgent('curl');

// Custom rule: block any request claiming to be from a specific bad actor
$firewall->rules->add('block-known-scanner', function ($request) {
 if (str_contains($request->userAgent, 'SpecificBadBot')) {
     return RuleResult::block('known scanner');
 }
 return RuleResult::pass();
});

Learning Mode

Before enabling blocking in production, there's a learning mode that logs all detected threats without blocking any requests:

XZeroProtect::init(['mode' => 'learning'])->run();

Running in learning mode for a few days gives you a realistic picture of what the default rules would block, and lets you tune patterns before they affect real users. This is especially useful when adding aggressive path patterns that might accidentally match legitimate traffic in your application.

Installation xZeroProtect

composer require webrium/xzeroprotect

The library requires PHP 8.0 or higher and write access to a storage directory. No other dependencies.

Source code and full documentation: xZeroProtect