Repository navigation
Home

A modular request firewall for PHP: local threat detection, IPv4/IPv6 reputation (AbuseIPDB.com and AbuseIPDB.ru), rate limiting, IP blocking with severity-based lifetimes, an incident journal and an admin dashboard.
One entry point, Deynekin\PhpShield\Middleware\Shield::inspect(), answers a single question: may
this request proceed, and if not, why?
| Layer | Class | Responsibility |
|---|---|---|
| Decision | Middleware\Shield |
rate limits, payload and header scanning, IP reputation, the verdict |
| State | Middleware\SecurityMiddleware |
blocks, incident journal, failed-attempt counters, cleanup, statistics |
| Detection | Detection\* |
pluggable rules that score a payload |
| Knowledge | ThreatIntel\* |
AbuseIPDB.com / AbuseIPDB.ru reputation with cache and circuit breaker |
Two policy rules shape every decision:
-
A single finding never bans anyone. Scores accumulate and only a total above
block_thresholdblocks. A permanent block is an operator decision, never a regex. - Nothing fails closed on infrastructure trouble. A database, Redis or API outage means "no data", not "deny".
inspect() walks the layers in a fixed order: cheap checks first, the network last, and a trusted
address leaves before any work is done. Every branch ends in the same decision array, so the caller
reads one shape whether the request was allowed or refused.
flowchart TD
REQ(["HTTP request"]) --> INSPECT["Shield::inspect()"]
INSPECT --> ENABLED{"enabled?"}
ENABLED -->|no| ALLOW(["allow"])
ENABLED -->|yes| TRUSTED{"client IP in trusted_ips?"}
TRUSTED -->|yes, never counted| ALLOW
TRUSTED -->|no| BLOCKED{"address blocked?"}
BLOCKED -->|yes| DENY403["403 ip_blocked"]
BLOCKED -->|no| FAILS{"failed attempts at limit?"}
FAILS -->|yes| BLOCK1["block once"]
BLOCK1 --> DENY403B["403 too_many_failures"]
FAILS -->|no| RATE{"rate limit exceeded?"}
RATE -->|yes| BLOCK2["block 300 s"]
BLOCK2 --> DENY429["429 rate_limited"]
RATE -->|no| REPUTATION["IP reputation: cache, provider fallback, breaker"]
REPUTATION --> SCORED{"abuse score"}
SCORED -->|block_score and above| BLOCK3["block + incident"]
BLOCK3 --> DENY403C["403 ip_reputation"]
SCORED -->|flag_score and above| JOURNAL1["journal only"]
JOURNAL1 --> SCAN["payload scan: 6 rules over GET, POST, REQUEST_URI"]
SCORED -->|below, no data or outage| SCAN
SCAN --> FINDING{"score"}
FINDING -->|block mode, threshold reached| BLOCK4["journal + block"]
BLOCK4 --> DENY403D["403 threat_detected"]
FINDING -->|challenge mode, threshold reached| DENY403E["403 challenge_required"]
FINDING -->|suspicious| JOURNAL2["journal once per 10 min"]
JOURNAL2 --> HEADERS["header scan: User-Agent"]
FINDING -->|clean| HEADERS
HEADERS -->|scanner| JOURNAL3["journal or block"]
HEADERS -->|clean| ALLOW
JOURNAL3 --> ALLOW
ALLOW --> DECISION["decision array: blocked, status, reason, detail, score, findings"]
DENY403 --> DECISION
DENY403B --> DECISION
DENY429 --> DECISION
DENY403C --> DECISION
DENY403D --> DECISION
DENY403E --> DECISION
DECISION --> APP(["application or error response"])
classDef allow fill:#d3f9d8,stroke:#2f9e44,color:#000000
classDef deny fill:#ffe3e3,stroke:#c92a2a,color:#000000
classDef step fill:#e7f5ff,stroke:#1c7ed6,color:#000000
class ALLOW,APP allow
class DENY403,DENY403B,DENY429,DENY403C,DENY403D,DENY403E deny
class INSPECT,SCAN,DECISION step
The mode branches sit next to the score, not at the top: monitor and challenge change the reaction
to a finding, not the order of the checks. The order itself is listed in
Getting started.
It is not a replacement for prepared statements, output escaping, CSP, authentication or session security. It buys time and visibility; it does not fix application defects.
composer require paulmann/php-shield$config = require 'vendor/paulmann/php-shield/config/default.php';
$config['mode'] = 'monitor'; // start safe
$shield = ShieldFactory::create($config, new mysqli($host, $user, $pass, $db));
$decision = $shield->inspect($_GET + $_POST, $_SERVER, $_SERVER['REQUEST_URI'] ?? '/');| If you want to… | Read |
|---|---|
| Install it and get a first decision | Getting started |
| Know what every configuration key does | Configuration reference |
| Understand or tune detection | Detection rules |
| Turn on AbuseIPDB reputation | IP reputation |
| Understand modes and block lifetimes | Modes and block lifetimes |
| Work with blocks, incidents and counters | State and storage |
| Choose a rate limiter | Rate limiting |
| Secure the admin dashboard | Admin dashboard |
| Run behind nginx, Cloudflare or a balancer | Reverse proxies and client IP |
| Deploy to production | Production checklist |
| Fix something that behaves oddly | Troubleshooting |
| Migrate from SecurityMiddleware 1.x | Upgrading |
| Understand the design | Architecture |
| Know what the security review found | Security |
| Run the tests and the analysis | Development |
- PHP 8.1 – 8.5,
ext-json,ext-mbstring - Optional:
ext-mysqli(MySQL storage and dashboard),ext-redis(Redis rate limiter),ext-curl(reputation lookups)
- Repository — README in six languages: English, Русский, Deutsch, Español, 中文, हिन्दी
- Architecture decision records
- Security review
- Reporting a vulnerability
MIT.
repository · releases · MIT