A mini web application firewall for PHP. Harden any PHP website in minutes — even on simple shared hosting.
AI crawlers, scrapers and automated attacks no longer hit only big sites. Every such request makes your website do real work — start the software, ask the database, build the page. Enough of them and the site slows down or goes offline, and junk requests can fill your page cache so that real visitors wait.
request-shield is a small gatekeeper that looks at every request before your website starts. Real visitors pass in a few millionths of a second. Everything else:
- Junk is turned away — scanners hunting for password files, broken or oversized requests never reach your site.
- Your cache stays clean — made-up addresses and parameters are answered, but never stored.
- Floods are slowed down — whoever asks too often has to wait; suspicious clients prove they are a real browser with an invisible check, no puzzles to click (how the browser check works, in plain words).
- Doors stay shut — an admin area only for your office, a form only where it belongs; your CMS can ask for the browser check when content is sent — or run it inside the form while the visitor types — and nobody loses what they typed.
- Unwelcome addresses stay out — keep an address out for a week with one command, let the office in, and ban for a while whoever keeps knocking (IP lists and automatic bans); a live view shows what is stopped right now, and why (live and lists).
- Search engines stay welcome — Google, Bing and others are recognised and let through.
- Readable rules — one per line, in a plain text file; every refusal names the line that caused it, and an optional log shows what was turned away.
- See what it does — a page shows the active rules in plain words, how often each one decided, and what happens to any address you try, step by step.
All parts at a glance, each with the one line that switches it: the parts of request-shield.
No extra server, no subscription, no data sent to anyone: one PHP library — upload, include, done.
What it is not: protection against attacks so large that they overload the network or the web server itself — that remains the job of your hoster or a CDN.
See it in 30 seconds: php -S 127.0.0.1:8080 examples/demo/router.php, then
open http://127.0.0.1:8080/ — a mini site with one example per feature,
including the invisible browser check (examples/demo).
Or the short tour: php -S 127.0.0.1:8090 examples/showcase/router.php — one
page, German and English, the rules beside what they mean, and every example a
real request you send yourself (examples/showcase).
It runs before the application — before the framework, its autoloader and its database — and decides in a few microseconds whether a request reaches the application, and whether the answer may be cached.
- Trusted proxies:
X-Forwarded-For,-Protoand-Hostare believed only from your load balancer or reverse proxy, and removed from$_SERVERotherwise, so neither the shield nor the application can be told another client, scheme or host. - Hard rejects: methods, sizes, broken or traversing paths, unknown hosts and
the paths only scanners ask for (
/.env,/.git/, backups,phpinfo.php, …) never reach the application. - A definition of what may be cached: URLs outside it are answered, but marked uncacheable, so random paths and parameters cannot fill a page cache.
- Budgets per client (an IPv4 address, an IPv6 /64): requests per window,
and budgets the application counts itself (cache misses, failed sign-ins).
Above a threshold a client can be challenged, above the limit it gets
429 Too Many RequestswithRetry-After. - Access rules: paths only for some addresses (
restrict /admin/** to …), methods only on some paths (allow POST /contact) — matched as the application routes the path, so//adminor/%61dmindo not get past. - Rule files and a log: the settings one rule per line, from several files
(a CMS extension ships its own); every decision names its rule
(
[SITE-10], orsite.rules:12); an optional log of what was stopped or flagged. The built-in blocks are rule files too (rules/), versioned; a site that changes one is told when an update changes it underneath. - No dependencies, no services: counters in APCu, or in plain files on hosting without APCu. PHP ≥ 8.0 (the Red Hat Enterprise Linux 9 baseline).
- Fail safe: whatever breaks inside the shield — a store it cannot write, an adapter's hook that throws — the request reaches the application, marked uncached, and PHP's error log gets one line a minute. A rule file that does not compile after a deploy leaves the last good rules in force. The shield's own answers (a refusal, the check page) are not affected (ADR 0007).
It turns an expensive request (framework, database, rendering: 100–200 ms) into a cheap one (well under 0.1 ms). It does not replace protection in front of PHP (the hoster's, a CDN's, CloudLinux, Imunify360): a flood still occupies web server and PHP slots, just very briefly.
- 0.1.0: the core.
- 0.2.0: the browser challenge (proof of work), settings checked once and compiled for OPcache, documentation, CI.
- 0.3.0: rule files, access rules, rule IDs, the log, the active rules page, attack rules, match blocks, the check inside the form, earning back a spent budget (forms and APIs), the demo; PHP 8.0.
- Unreleased: known query parameters and their types,
query strict,@tracking(docs); modes — monitor first, strict under attack,monitorfor single rules,challenge … max-age(docs); known crawlers — search engines and AI crawlers verified by their published address lists or DNS, allowed, checked or refused per kind (docs); statistics — requests, rules, status codes, pages not found and who links to them, what each crawler did — kept per hour,bin/request-shield stats(docs); plugins — the core is the firewall, the statistics are its first plugin, and your own hang on the same two hooks (docs); rules per website (siteblocks); IP lists —deny,exempt … until, list files kept from the command line — and automatic, temporary bans (docs); the live view and the lists in the dashboard — what is stopped right now, with the reason and where it came from, an address kept out with one click and a comment (docs); public blocklists (Spamhaus DROP, DShield, blocklist.de, Tor, cloud ranges …) fetched by cron, each with its own action, and exported for a firewall (docs). - Next: adapters for Exponential, WordPress and Ibexa; exporting the rules to nginx, Apache and Varnish.
Documentation: docs/ — features, use cases, proposals, architecture decisions. Privacy and the GDPR: docs/privacy.md. Changes: CHANGELOG.md.
Three steps: put the directory somewhere outside the document root, write
the rules to request-shield.rules next to bootstrap.php (or to
config/request-shield.rules; a PHP array in config/request-shield.php
works too, see config/request-shield.dist.php), and prepend it:
; .user.ini in the document root (PHP-FPM, LiteSpeed LSAPI)
auto_prepend_file = /home/you/request-shield/bootstrap.php# .htaccess (Apache mod_php, LiteSpeed)
php_value auto_prepend_file /home/you/request-shield/bootstrap.phpphp bin/request-shield check request-shield.rules tells you the rules are
in order, and what this hosting can do (the tier: APCu, files, or stateless).
The shield keeps its compiled settings, counters and secret in
.request-shield/ next to the rules — which is why both belong outside the
document root. The settings file can also be named by a constant or an environment
variable, REQUEST_SHIELD_CONFIG (a .rules or a .php file; then nothing
else is looked for). Without any settings file the shield does nothing.
composer require cjw-network/request-shieldand call it first thing in the front controller:
CjwNetwork\RequestShield\Shield::protectFile(__DIR__ . '/../config/request-shield.rules');protectFile() checks the settings once and keeps them compiled for OPcache;
protect($array) checks them on every call. An adapter passes its
extensions' rule files as sources.
- WordPress: at the top of
wp-config.php, or asauto_prepend_file. - Ibexa / Symfony: at the top of
public/index.php. - Exponential: in
config.php, before the HTTP cache's early exit.
As a rule file (all rules):
trust 10.0.0.0/8
host www.example.org example.org
cache-query page
limit requests 600/min
limit misses 60/min on-demand
restrict /admin/** to 192.0.2.0/24
challenge /login
set log /var/log/request-shield.log
php bin/request-shield check|show|reload site.rules checks it, shows the
rules in effect with their origins, or makes every server read it again;
trace site.rules "GET https://…/wp-login.php" shows what happens to a
request, check by check; version [site.rules] says what is installed (the
version, PHP, APCu, the store in use, the rule sets' versions). The same as a
page for the admin area: the active rules page.
Or as a PHP array — every key, with its default, is in src/Config.php;
config/request-shield.dist.php is a starting point:
return [
'trustedProxies' => ['10.0.0.0/8'],
'hosts' => ['www.example.org', 'example.org'],
'cacheable' => ['query' => ['page'], 'paths' => null],
'budgets' => [
'requests' => ['limit' => 600, 'window' => 60],
'misses' => ['limit' => 60, 'window' => 60, 'onDemand' => true],
],
];$decision = CjwNetwork\RequestShield\Shield::current(); // or $_SERVER['REQUEST_SHIELD']
if ($decision && !$decision->cacheable()) {
// answer normally, but do not store the page
}A page cache that misses can count the miss against the client:
// after protect()/protectFile(): the same settings and request
if (!CjwNetwork\RequestShield\Shield::active()->consume('misses')->passes()) {
// too many renders from this client: answer 429, or a stale copy
}php -d apc.enable_cli=1 bench/overhead.php — a passing request with eleven
headers behind a trusted proxy, every check on:
| PHP 8.1 | per passing request |
|---|---|
| checks, APCu store (tier S2) | ~12 µs |
| checks, file store (tier S1, the shared-hosting norm) | ~42 µs |
| settings: rule files (compiled, APCu) / PHP file | ~5.5 / ~8 µs |
| settings compiled on every request (tier S0: nowhere to write) | + ~30 µs |
| challenge page / solution check / pass cookie (challenged clients only) | ~12 / ~9 / ~5 µs |
The tiers (settings):
PHP ≥ 8.0 is the only requirement; a writable directory and APCu make it
faster. request-shield check site.rules says which tier a hosting gives.
php tests/run.php # no framework needed, PHP 8.0+
composer install && composer phpstan && composer taintThe tests include an end-to-end run through PHP's built-in server with
auto_prepend_file, and the challenge page's own script run in Node against
the PHP check. CI runs them on every supported PHP version, with and without
APCu, plus PHPStan (level max) and Psalm's taint analysis.
See CONTRIBUTING.md (and AGENTS.md for AI coding agents). Security issues: SECURITY.md.
Copyright (C) 2026 JAC Systeme GmbH, part of CJW Network.
Released under the MIT license, see LICENSE.