diff --git a/docs/advanced/discriminator-normalizer.md b/docs/advanced/discriminator-normalizer.md index fe1966d..c56e746 100644 --- a/docs/advanced/discriminator-normalizer.md +++ b/docs/advanced/discriminator-normalizer.md @@ -89,7 +89,7 @@ This ensures: | `ip-limit` | `ip-limit` | | `my rule with spaces` | `my_rule_with_spaces` | | (empty string) | `empty` | -| (very long name) | `first_107_chars...-a1b2c3d4e5f6` | +| (name over 120 chars) | `first_107_chars...-a1b2c3d4e5f6` | | User Key | Cache Key Suffix | |----------|-----------------| @@ -210,6 +210,6 @@ The prefix is validated: it is trimmed (whitespace and a trailing `:` stripped), 2. **Normalize application-level keys yourself.** The discriminator normalizer handles global concerns (case, trim). Domain-specific normalization (like email deduplication) should happen in your key closure. -3. **Use consistent key structures.** When writing custom key closures, prefix your keys to avoid collisions between different rule types: `user:123` instead of just `123`. +3. **Use consistent key structures.** When writing custom key closures, prefix your keys to avoid collisions between different rule types: `user:123` instead of `123`. 4. **Avoid sensitive data in keys.** While user keys are SHA-256 hashed in cache, the raw key is still visible in event payloads (`TrackHit`, `ThrottleExceeded`, etc.). Use hashed or anonymized identifiers when possible. diff --git a/docs/advanced/dynamic-throttle.md b/docs/advanced/dynamic-throttle.md index 907fad9..cc82e38 100644 --- a/docs/advanced/dynamic-throttle.md +++ b/docs/advanced/dynamic-throttle.md @@ -4,7 +4,7 @@ outline: deep # Dynamic Throttle & Advanced Rate Limiting -Phirewall's throttle system goes beyond simple fixed-window rate limiting. This page covers dynamic limits, sliding windows, multi-window throttling, and advanced patterns for building fine-grained rate limiting strategies. +Phirewall's throttle system does more than fixed-window rate limiting. This page covers dynamic limits, sliding windows, multi-window throttling, and patterns for fine-grained rate limiting. For basic rate limiting setup, see [Rate Limiting](/features/rate-limiting). @@ -18,7 +18,6 @@ Give different users different quotas based on a request header: ```php use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; use Psr\Http\Message\ServerRequestInterface; @@ -323,7 +322,7 @@ $config->throttles->add('api-limit', ``` ::: tip -For trusted traffic that should bypass **all** rules (not just throttles), use [safelists](/features/safelists-blocklists) instead. Safelisted requests skip the entire firewall pipeline, including blocklists, fail2ban, and track rules. +For trusted traffic that should bypass **all** rules (not only throttles), use [safelists](/features/safelists-blocklists) instead. Safelisted requests skip the entire firewall pipeline, including blocklists, fail2ban, and track rules. ::: ## Database-Driven Key Assignment @@ -336,7 +335,7 @@ $userTiers = $db->fetchAll('SELECT user_id, plan FROM users'); $tierMap = array_column($userTiers, 'plan', 'user_id'); $config->throttles->add('db-tiered', - limit: fn(ServerRequestInterface $request) use ($tierMap): int => + limit: fn(ServerRequestInterface $request): int => match ($tierMap[$request->getAttribute('userId') ?? ''] ?? 'anonymous') { 'enterprise' => 10000, 'pro' => 1000, @@ -375,9 +374,13 @@ $firewall->resetThrottle('api:p60', '192.168.1.100'); $firewall->resetAll(); ``` +::: warning +`resetThrottle()` clears fixed-window, multi-window, and dynamic-period counters only. It does **not** reset sliding-window counters created via `throttles->sliding(...)`: sliding windows are stored under per-window keys (suffixed `.w.{windowStart}`) rather than the key `resetThrottle()` deletes. To clear a sliding-window counter, call `resetAll()` or let the windows expire (TTL = 2 x period). +::: + ## Best Practices -1. **Use descriptive rule names.** Names appear in `X-RateLimit-*` headers, `ThrottleExceeded` events, and (when `enableResponseHeaders()` is active) `X-Phirewall-Matched` headers. Use `api-free-tier` instead of `rule1`. +1. **Use descriptive rule names.** Names appear in `ThrottleExceeded` events and, when `enableResponseHeaders()` is active, in the `X-Phirewall-Matched` response header. (The `X-RateLimit-*` headers carry only numeric limit/remaining/reset values, not the rule name.) Use `api-free-tier` instead of `rule1`. 2. **Return `null` to skip.** This is the primary mechanism for conditional rate limiting. When a key closure returns `null`, the rule is skipped with zero overhead. diff --git a/docs/advanced/infrastructure.md b/docs/advanced/infrastructure.md index dddc566..f2e552d 100644 --- a/docs/advanced/infrastructure.md +++ b/docs/advanced/infrastructure.md @@ -143,7 +143,7 @@ new InfrastructureBanListener( | `$blockOnFail2Ban` | `bool` | `true` | Mirror Fail2Ban bans | | `$blockOnBlocklist` | `bool` | `false` | Mirror blocklist hits (request IP) | | `$keyToIp` | `?callable` | identity | Map a Fail2Ban key to an IP (default: assumes key is an IP) | -| `$requestToIp` | `?callable` | `KeyExtractors::ip()` | Extract IP from a `ServerRequestInterface` | +| `$requestToIp` | `?callable` | `REMOTE_ADDR` (raw peer) | Extract IP from a `ServerRequestInterface` | ### Wiring with PSR-14 @@ -383,7 +383,6 @@ A full setup combining Fail2Ban, infrastructure mirroring, and rate limiting: declare(strict_types=1); use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Store\RedisCache; use Flowd\Phirewall\Infrastructure\ApacheHtaccessAdapter; diff --git a/docs/advanced/observability.md b/docs/advanced/observability.md index d4c9e62..04c8c90 100644 --- a/docs/advanced/observability.md +++ b/docs/advanced/observability.md @@ -4,7 +4,7 @@ outline: deep # Observability -Phirewall provides comprehensive observability through PSR-14 (PHP Standard Recommendation for Event Dispatching) events and built-in diagnostics counters. Every significant decision the firewall makes is observable, making it straightforward to integrate with any logging, metrics, or alerting system you already use. +Phirewall provides observability through PSR-14 (PHP Standard Recommendation for Event Dispatching) events and built-in diagnostics counters. Every significant decision the firewall makes is observable, so you can integrate it with any logging, metrics, or alerting system you already use. ## Enabling Events @@ -263,7 +263,7 @@ Access the counters from the dispatcher at any time via `$dispatcher->counters() ### Minimal Dispatcher -The simplest possible dispatcher for quick debugging: +A minimal dispatcher for quick debugging: ```php use Psr\EventDispatcher\EventDispatcherInterface; @@ -279,7 +279,7 @@ $dispatcher = new class implements EventDispatcherInterface { ### Monolog Integration -Full-featured logging with different severity levels for different event types: +Logging with different severity levels for different event types: ```php use Monolog\Logger; diff --git a/docs/advanced/portable-config.md b/docs/advanced/portable-config.md index 7b0861a..a58725e 100644 --- a/docs/advanced/portable-config.md +++ b/docs/advanced/portable-config.md @@ -4,7 +4,7 @@ outline: deep # Portable Config -`PortableConfig` expresses a firewall ruleset as plain, JSON-serializable data instead of PHP closures. Because a ruleset is just data, you can: +`PortableConfig` expresses a firewall ruleset as plain, JSON-serializable data instead of PHP closures. Because a ruleset is data, you can: - **store it in a database** and pick up rule changes on the next request, - **ship it through a config service** (etcd, Consul, S3, a settings table), @@ -95,14 +95,18 @@ Everything `PortableConfig` can express today. | Factory | Keys on | |---------|---------| -| `keyIp()` | client IP (`REMOTE_ADDR`) | +| `keyIp()` | client IP resolved via the Config's IP resolver (else `REMOTE_ADDR`), like keyless rules and `filterIp()` | | `keyMethod()` | HTTP method | | `keyPath()` | request path | | `keyHeader(name)` | raw value of header `name` | | `keyHashedHeader(name)` | sha256 fingerprint of header `name`, preferred for credential-bearing headers (`Authorization`, `Cookie`, `X-Api-Key`) so the raw value never reaches the cache/ban registry | ::: tip -`keyIp()` keys on `REMOTE_ADDR`, which behind a CDN or load balancer is the proxy's address, not the client's. The IP resolver is a closure and therefore not portable; set it on the rebuilt `Config` with `setIpResolver(KeyExtractors::clientIp(new TrustedProxyResolver([...])))`. See [Client IP behind proxies](/getting-started#client-ip-behind-proxies). +`keyIp()` is resolver-aware: it materializes as a keyless rule and late-binds to the evaluating `Config`'s IP resolver at runtime, exactly like keyless counter rules and `filterIp()`. Behind a CDN or load balancer, set proxy trust once on the `Config` with `$config->setIpResolver((new TrustedProxyResolver([...]))->resolve(...))` and all `keyIp()`-keyed rules (plus keyless rules and `filterIp()`) will key on the resolved client IP automatically. Without a resolver, `REMOTE_ADDR` is used. +::: + +::: warning +Never trust `X-Forwarded-For` or similar headers without configuring trusted proxies via `TrustedProxyResolver`. A `TrustedProxyResolver` only reads forwarded headers when the connecting peer matches a declared trusted proxy address; without that, a client can spoof any IP by sending their own `X-Forwarded-For` header. See [Client IP behind proxies](/getting-started#client-ip-behind-proxies). ::: ### Pattern kinds (`PortableConfig::patternEntry()`) diff --git a/docs/advanced/presets.md b/docs/advanced/presets.md index f038ec1..c074af7 100644 --- a/docs/advanced/presets.md +++ b/docs/advanced/presets.md @@ -41,7 +41,7 @@ Resolve any preset by name with `Presets::get($name)` (a `PortableConfig`), pass ## Conventions and overrides -- The shipped presets target signals that are universal across applications (scanner User-Agents, missing browser headers, well-known sensitive paths), so they assume nothing about your routing. A preset you build yourself is just a `PortableConfig`, so it can key on whatever fits your environment, including routes your own apps standardize. +- The shipped presets target signals that are universal across applications (scanner User-Agents, missing browser headers, well-known sensitive paths), so they assume nothing about your routing. A preset you build yourself is a `PortableConfig`, so it can key on whatever fits your environment, including routes your own apps standardize. - Override any rule by applying the preset with your own portable rules that redefine the rule by the same name (later layer wins), or by rebuilding the preset's schema. > **Note:** `scannerBlocking()`'s `suspicious-headers` rule is the more aggressive of the two: some legitimate API clients, privacy tools, and embedded browsers also omit `Accept-*` headers. Drop or override it by name if your traffic includes non-browser clients. diff --git a/docs/advanced/psr17.md b/docs/advanced/psr17.md index 0d877b1..36762a3 100644 --- a/docs/advanced/psr17.md +++ b/docs/advanced/psr17.md @@ -313,7 +313,6 @@ A full example combining both approaches: use Flowd\Phirewall\Config; use Flowd\Phirewall\Config\Response\Psr17BlocklistedResponseFactory; use Flowd\Phirewall\Config\Response\Psr17ThrottledResponseFactory; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Store\InMemoryCache; use Nyholm\Psr7\Factory\Psr17Factory; @@ -421,7 +420,6 @@ class PhirewallFactory ```php [Laravel] // In a service provider use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Store\ApcuCache; use Nyholm\Psr7\Factory\Psr17Factory; diff --git a/docs/advanced/request-context.md b/docs/advanced/request-context.md index 5926bee..a6a74ad 100644 --- a/docs/advanced/request-context.md +++ b/docs/advanced/request-context.md @@ -19,7 +19,7 @@ $config->fail2ban->add('login', ); ``` -With `RequestContext`, your handler verifies the credentials first, then signals a failure **only when authentication actually fails**. This gives you precise control over what counts as a failure. +With `RequestContext`, your handler verifies the credentials first, then signals a failure **only when authentication fails**. This gives you precise control over what counts as a failure. ## How It Works @@ -51,7 +51,6 @@ Configure a fail2ban rule with a filter that **always returns `false`**. This me ```php use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Store\InMemoryCache; use Psr\Http\Message\ServerRequestInterface; @@ -80,6 +79,7 @@ Retrieve the `RequestContext` from the request attribute and call `recordFailure ```php use Flowd\Phirewall\Context\RequestContext; +use Nyholm\Psr7\Response; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -88,8 +88,9 @@ class LoginHandler implements RequestHandlerInterface { public function handle(ServerRequestInterface $request): ResponseInterface { - $username = $request->getParsedBody()['username'] ?? ''; - $password = $request->getParsedBody()['password'] ?? ''; + $body = (array) $request->getParsedBody(); + $username = $body['username'] ?? ''; + $password = $body['password'] ?? ''; if (!$this->authenticate($username, $password)) { // Retrieve the RequestContext attached by the middleware @@ -100,10 +101,10 @@ class LoginHandler implements RequestHandlerInterface // rule's own keyExtractor. Use the null-safe operator for safety. $context?->recordFailure('login-failures'); - return new JsonResponse(['error' => 'Invalid credentials'], 401); + return new Response(401, ['Content-Type' => 'application/json'], json_encode(['error' => 'Invalid credentials'], JSON_THROW_ON_ERROR)); } - return new JsonResponse(['success' => true, 'user' => $username], 200); + return new Response(200, ['Content-Type' => 'application/json'], json_encode(['success' => true, 'user' => $username], JSON_THROW_ON_ERROR)); } } ``` @@ -125,7 +126,6 @@ The first parameter to `recordFailure()` must **exactly** match the `name` you u First, configure an allow2ban rule. To make the rule count *only* the events recorded by the handler (not every request), have the rule's `keyExtractor` return `null` pre-handler; the firewall then skips counting until the handler signals an explicit key via `recordHit()`: ```php -use Flowd\Phirewall\KeyExtractors; $config->allow2ban->add( 'expensive-endpoint', @@ -154,7 +154,7 @@ If the rule's `keyExtractor` returns a value pre-handler (the common case), the $context?->recordHit('expensive-endpoint'); ``` -Note that when the rule's `keyExtractor` returns a value pre-handler, **both** the pre-handler counter and the handler's `recordHit()` increment the counter, so the threshold should account for the doubled count. +When the rule's `keyExtractor` returns a value pre-handler, **both** the pre-handler counter and the handler's `recordHit()` increment the counter, so the threshold should account for the doubled count. Recorded failures and hits are processed together after your handler returns; retrieve them all with `getRecordedSignals()`. @@ -212,7 +212,7 @@ if ($context !== null) { $result->outcome->value; // 'pass', 'safelisted', etc. $result->isPass(); // true if the request was allowed through - $result->rule; // Name of the matching rule (null if simply passed) + $result->rule; // Name of the matching rule (null if the request passed) } ``` @@ -246,7 +246,6 @@ require __DIR__ . '/vendor/autoload.php'; use Flowd\Phirewall\Config; use Flowd\Phirewall\Context\RequestContext; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Store\InMemoryCache; use Nyholm\Psr7\Factory\Psr17Factory; @@ -359,7 +358,6 @@ use Flowd\Phirewall\BanType; use Flowd\Phirewall\Config; use Flowd\Phirewall\Context\RequestContext; use Flowd\Phirewall\Http\Firewall; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Store\InMemoryCache; use Nyholm\Psr7\Factory\Psr17Factory; diff --git a/docs/advanced/track-notifications.md b/docs/advanced/track-notifications.md index 223e2dc..7440d4e 100644 --- a/docs/advanced/track-notifications.md +++ b/docs/advanced/track-notifications.md @@ -8,7 +8,7 @@ Track rules provide **passive counting without blocking**. They are ideal for ob ## How Tracking Works -Track rules are evaluated **first** in the pipeline, before safelists and blocklists. They always run, even for requests that will be safelisted. This makes them reliable for comprehensive monitoring. +Track rules are evaluated **first** in the pipeline, before safelists and blocklists. They always run, even for requests that will be safelisted. This makes them reliable for monitoring all traffic. ```text Request --> Track (passive) --> Safelist --> Blocklist --> Fail2Ban --> Throttle --> Allow2Ban --> Pass @@ -62,7 +62,6 @@ $config->tracks Monitor login attempts per IP for dashboards and anomaly detection: ```php -use Flowd\Phirewall\KeyExtractors; $config->tracks->add('login-attempts', period: 3600, @@ -87,7 +86,6 @@ $config->tracks->add('api-usage', Monitor access to admin or configuration pages: ```php -use Flowd\Phirewall\KeyExtractors; $config->tracks->add('admin-access', period: 600, diff --git a/docs/common-attacks.md b/docs/common-attacks.md index c411ba6..486a5f9 100644 --- a/docs/common-attacks.md +++ b/docs/common-attacks.md @@ -363,7 +363,7 @@ $config->allow2ban->add('volume-ban', ### API Endpoint Throttling -Rate-limit API traffic per client IP, the value a caller cannot forge (behind a proxy, resolve it with `KeyExtractors::clientIp()` and a `TrustedProxyResolver`): +Rate-limit API traffic per client IP, the value a caller cannot forge (behind a proxy, configure proxy trust once with `$config->setIpResolver((new TrustedProxyResolver([...]))->resolve(...))` and rules key on the resolved client IP by default): ```php $config->throttles->add('api', @@ -373,7 +373,7 @@ $config->throttles->add('api', ``` ::: warning Header keys are client-controlled -A throttle, fail2ban, or allow2ban rule keyed on a request header (`X-Api-Key`, `X-User-Id`, …) is only as trustworthy as that header. A client can rotate or drop the header to land in a fresh counter on every request and never reach the threshold (a trivial bypass). Key such rules on a value the client cannot freely change: the client IP (via `KeyExtractors::clientIp()` with a `TrustedProxyResolver`), the authenticated principal your auth layer sets *after* verifying it, or a composite of both. When you must key on a credential-bearing header, use `KeyExtractors::hashedHeader('X-Api-Key')`: the raw value otherwise reaches the ban registry and event payloads (and your logs) in cleartext. +A throttle, fail2ban, or allow2ban rule keyed on a request header (`X-Api-Key`, `X-User-Id`, …) is only as trustworthy as that header. A client can rotate or drop the header to land in a fresh counter on every request and never reach the threshold (a trivial bypass). Key such rules on a value the client cannot freely change: the client IP (keyless rules key on the resolved client IP by default; set proxy trust with `$config->setIpResolver((new TrustedProxyResolver([...]))->resolve(...))`), the authenticated principal your auth layer sets *after* verifying it, or a composite of both. When you must key on a credential-bearing header, use `KeyExtractors::hashedHeader('X-Api-Key')`: the raw value otherwise reaches the ban registry and event payloads (and your logs) in cleartext. ::: ### Expensive Endpoint Protection @@ -408,15 +408,14 @@ $config->tracks->add('sensitive-endpoints', When the count reaches 50, a `TrackHit` event is dispatched with `thresholdReached: true`. See [Track & Notifications](/advanced/track-notifications) for details. -## Comprehensive Production Setup +## Production Setup -Combine all layers into a production-ready configuration: +Combine all layers into a single production configuration: ```php use Flowd\Phirewall\Config; use Flowd\Phirewall\Config\Rule\SafelistRule; use Flowd\Phirewall\Http\TrustedProxyResolver; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Matchers\TrustedBotMatcher; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Config\Rule\BlocklistRule; @@ -432,13 +431,13 @@ $config->enableRateLimitHeaders(); // Trusted proxy for correct client IP resolution $proxy = new TrustedProxyResolver(['10.0.0.0/8', '172.16.0.0/12']); -$config->setIpResolver(KeyExtractors::clientIp($proxy)); +$config->setIpResolver($proxy->resolve(...)); // ── Layer 1: Safelists ───────────────────────────────────────────────── $config->safelists->add('health', fn($req): bool => $req->getUri()->getPath() === '/health' ); -$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(ipResolver: $config->getIpResolver(), cache: new RedisCache($redis)))); +$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(cache: new RedisCache($redis)))); $config->safelists->ip('office', ['203.0.113.0/24']); // ── Layer 2: Blocklists ──────────────────────────────────────────────── @@ -516,7 +515,7 @@ Track → Safelist → Blocklist → Fail2Ban → Throttle → Allow2Ban → Pas 2. **Safelist your health checks.** Internal monitoring endpoints should bypass all firewall rules to avoid false alerts. -3. **Use `clientIp()` behind proxies.** If your application runs behind a load balancer or CDN, configure a `TrustedProxyResolver` so rate limits and bans apply to the real client IP; raw `KeyExtractors::ip()` would collapse every client onto the proxy's address. See [Client IP Behind Proxies](/getting-started#client-ip-behind-proxies). +3. **Configure proxy trust on the Config.** If your application runs behind a load balancer or CDN, set `$config->setIpResolver((new TrustedProxyResolver([...]))->resolve(...))` so rate limits and bans apply to the real client IP; keyless rules then key on the resolved client IP automatically. Avoid keying rules on the raw `REMOTE_ADDR` peer address directly - behind a proxy it collapses every client onto the proxy's address. If you need the raw peer for a specific purpose, read `$request->getServerParams()['REMOTE_ADDR']` directly. See [Client IP Behind Proxies](/getting-started#client-ip-behind-proxies). 4. **Start with logging, then enforce.** Use [Track rules](/advanced/track-notifications) to observe traffic patterns before enabling blocking rules. diff --git a/docs/examples.md b/docs/examples.md index 665f67c..137e971 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -8,7 +8,7 @@ Complete, copy-pasteable configurations for common scenarios. Each example is se ## Running the Built-in Examples -The Phirewall repository includes 31 runnable examples: +The Phirewall repository includes 28 runnable examples: ```bash git clone https://github.com/flowd/phirewall @@ -22,17 +22,14 @@ php examples/01-basic-setup.php | 01 | [basic-setup](https://github.com/flowd/phirewall/blob/main/examples/01-basic-setup.php) | Minimal configuration to get started | | 02 | [brute-force-protection](https://github.com/flowd/phirewall/blob/main/examples/02-brute-force-protection.php) | Fail2Ban-style login protection | | 03 | [api-rate-limiting](https://github.com/flowd/phirewall/blob/main/examples/03-api-rate-limiting.php) | Tiered rate limits for APIs | -| 04 | [sql-injection-blocking](https://github.com/flowd/phirewall/blob/main/examples/04-sql-injection-blocking.php) | OWASP-style SQLi detection | -| 05 | [xss-prevention](https://github.com/flowd/phirewall/blob/main/examples/05-xss-prevention.php) | Cross-Site Scripting protection | | 06 | [bot-detection](https://github.com/flowd/phirewall/blob/main/examples/06-bot-detection.php) | Scanner and malicious bot blocking | | 07 | [ip-blocklist](https://github.com/flowd/phirewall/blob/main/examples/07-ip-blocklist.php) | File-backed IP/CIDR blocklists | -| 08 | [comprehensive-protection](https://github.com/flowd/phirewall/blob/main/examples/08-comprehensive-protection.php) | Production-ready multi-layer setup | +| 08 | [comprehensive-protection](https://github.com/flowd/phirewall/blob/main/examples/08-comprehensive-protection.php) | Multi-layer production setup | | 09 | [observability-monolog](https://github.com/flowd/phirewall/blob/main/examples/09-observability-monolog.php) | Event logging with Monolog | | 10 | [observability-opentelemetry](https://github.com/flowd/phirewall/blob/main/examples/10-observability-opentelemetry.php) | Distributed tracing with OpenTelemetry | | 11 | [redis-storage](https://github.com/flowd/phirewall/blob/main/examples/11-redis-storage.php) | Redis backend for multi-server deployments | | 12 | [apache-htaccess](https://github.com/flowd/phirewall/blob/main/examples/12-apache-htaccess.php) | Apache .htaccess IP blocking | | 13 | [benchmarks](https://github.com/flowd/phirewall/blob/main/examples/13-benchmarks.php) | Storage backend performance comparison | -| 14 | [owasp-crs-files](https://github.com/flowd/phirewall/blob/main/examples/14-owasp-crs-files.php) | Loading OWASP CRS rules from files | | 15 | [in-memory-pattern-backend](https://github.com/flowd/phirewall/blob/main/examples/15-in-memory-pattern-backend.php) | Configuration-based CIDR/IP blocklists | | 16 | [allow2ban](https://github.com/flowd/phirewall/blob/main/examples/16-allow2ban.php) | Volume-based banning (inverse of fail2ban) | | 17 | [known-scanners](https://github.com/flowd/phirewall/blob/main/examples/17-known-scanners.php) | Block known attack tools by User-Agent | @@ -51,11 +48,21 @@ php examples/01-basic-setup.php | 30 | [config-composition](https://github.com/flowd/phirewall/blob/main/examples/30-config-composition.php) | Layering configs (vendor → environment → tenant → deployment) | | 31 | [presets](https://github.com/flowd/phirewall/blob/main/examples/31-presets.php) | Ready-to-use rule presets and version comparison (compare `Presets::VERSION` against your own release feed) | +### Companion Preset Packages + +Three companion packages ship ready-made presets, each with its own documentation and runnable examples. The former SQLi, XSS, and CRS-file examples (04, 05, 14) live in the OWASP CRS package now. + +| Package | Provides | +|---------|----------| +| [flowd/phirewall-preset-owasp-crs](https://github.com/flowd/phirewall-preset-owasp-crs) | OWASP CRS SecRule engine plus per-paranoia-level blocklist and fail2ban presets ([docs](/features/owasp-crs)) | +| [flowd/phirewall-preset-bots](https://github.com/flowd/phirewall-preset-bots) | Block AI crawlers and rate-limit aggressive SEO bots with curated User-Agent presets ([docs](/features/bot-presets)) | +| [flowd/phirewall-preset-bad-ips](https://github.com/flowd/phirewall-preset-bad-ips) | Block known-bad IPs using a bundled threat-intelligence feed snapshot ([docs](/features/bad-ip-preset)) | + --- ## Framework Integration -Production-ready integration examples for popular PHP frameworks. Each example includes storage, safelists, blocklists, rate limiting, brute-force protection, OWASP rules, and observability. Copy, paste, adapt. +Integration examples for popular PHP frameworks. Each example includes storage, safelists, blocklists, rate limiting, brute-force protection, OWASP rules, and observability. Copy, paste, adapt. ::: tip OWASP CRS is a separate package The OWASP rules in these examples use the companion package. Install it first: @@ -76,7 +83,6 @@ require __DIR__ . '/vendor/autoload.php'; use Flowd\Phirewall\Config; use Flowd\Phirewall\Config\Rule\SafelistRule; use Flowd\Phirewall\Http\TrustedProxyResolver; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Matchers\TrustedBotMatcher; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Config\Rule\BlocklistRule; @@ -107,7 +113,7 @@ $proxyResolver = new TrustedProxyResolver([ '172.16.0.0/12', '192.168.0.0/16', ]); -$config->setIpResolver(KeyExtractors::clientIp($proxyResolver)); +$config->setIpResolver($proxyResolver->resolve(...)); // ── Safelists ──────────────────────────────────────────────────────── $config->safelists->add('health', @@ -119,7 +125,7 @@ $config->safelists->add('metrics', $req->getUri()->getPath() === '/metrics' ); $config->safelists->ip('office', ['10.0.0.0/8', '192.168.1.0/24']); -$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(ipResolver: $config->getIpResolver(), cache: $cache))); +$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(cache: $cache))); // ── Blocklists ─────────────────────────────────────────────────────── $config->blocklists->knownScanners(); @@ -145,23 +151,19 @@ $config->fail2ban->add('login-abuse', filter: fn(ServerRequestInterface $req): bool => $req->getMethod() === 'POST' && $req->getUri()->getPath() === '/login', - key: KeyExtractors::clientIp($proxyResolver) ); // ── Rate Limiting ──────────────────────────────────────────────────── $config->throttles->add('burst', limit: 30, period: 5, - key: KeyExtractors::clientIp($proxyResolver) ); $config->throttles->add('global', limit: 1000, period: 60, - key: KeyExtractors::clientIp($proxyResolver) ); // ── Allow2Ban ──────────────────────────────────────────────────────── $config->allow2ban->add('flood-protection', threshold: 500, period: 60, banSeconds: 3600, - key: KeyExtractors::clientIp($proxyResolver) ); // ── PSR-17 Response Bodies ─────────────────────────────────────────── @@ -214,7 +216,6 @@ namespace App\Factory; use Flowd\Phirewall\Config; use Flowd\Phirewall\Config\Rule\SafelistRule; use Flowd\Phirewall\Http\TrustedProxyResolver; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Matchers\TrustedBotMatcher; use Flowd\Phirewall\Middleware as PhirewallMiddleware; use Flowd\Phirewall\Config\Rule\BlocklistRule; @@ -250,9 +251,7 @@ class PhirewallFactory $trustedProxies = array_values(array_filter($this->trustedProxies)); if ($trustedProxies !== []) { $proxyResolver = new TrustedProxyResolver($trustedProxies); - $config->setIpResolver( - KeyExtractors::clientIp($proxyResolver) - ); + $config->setIpResolver($proxyResolver->resolve(...)); } // ── Safelists ──────────────────────────────────────────── @@ -264,7 +263,7 @@ class PhirewallFactory fn(ServerRequestInterface $req): bool => str_starts_with($req->getUri()->getPath(), '/_profiler') ); - $config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(ipResolver: $config->getIpResolver(), cache: $cache))); + $config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(cache: $cache))); // ── Blocklists ─────────────────────────────────────────── $config->blocklists->knownScanners(); @@ -443,7 +442,6 @@ namespace App\Providers; use Flowd\Phirewall\Config; use Flowd\Phirewall\Config\Rule\SafelistRule; use Flowd\Phirewall\Http\TrustedProxyResolver; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Matchers\TrustedBotMatcher; use Flowd\Phirewall\Middleware as PhirewallMiddleware; use Flowd\Phirewall\Config\Rule\BlocklistRule; @@ -488,9 +486,7 @@ class PhirewallServiceProvider extends ServiceProvider $trustedProxies = array_filter(explode(',', (string) env('TRUSTED_PROXIES', ''))); if ($trustedProxies !== []) { $proxyResolver = new TrustedProxyResolver($trustedProxies); - $config->setIpResolver( - KeyExtractors::clientIp($proxyResolver) - ); + $config->setIpResolver($proxyResolver->resolve(...)); } // ── Safelists ──────────────────────────────────────── @@ -502,7 +498,7 @@ class PhirewallServiceProvider extends ServiceProvider fn(ServerRequestInterface $req): bool => str_starts_with($req->getUri()->getPath(), '/horizon') ); - $config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(ipResolver: $config->getIpResolver(), cache: $cache))); + $config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(cache: $cache))); // ── Blocklists ─────────────────────────────────────── $config->blocklists->knownScanners(); @@ -680,7 +676,6 @@ require __DIR__ . '/vendor/autoload.php'; use Flowd\Phirewall\Config; use Flowd\Phirewall\Config\Rule\SafelistRule; use Flowd\Phirewall\Http\TrustedProxyResolver; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Matchers\TrustedBotMatcher; use Flowd\Phirewall\Middleware as PhirewallMiddleware; use Flowd\Phirewall\Config\Rule\BlocklistRule; @@ -704,14 +699,14 @@ $config->setFailOpen(true); // ── Trusted Proxies ────────────────────────────────────────────────── $proxyResolver = new TrustedProxyResolver(['10.0.0.0/8', '172.16.0.0/12']); -$config->setIpResolver(KeyExtractors::clientIp($proxyResolver)); +$config->setIpResolver($proxyResolver->resolve(...)); // ── Safelists ──────────────────────────────────────────────────────── $config->safelists->add('health', fn(ServerRequestInterface $req): bool => $req->getUri()->getPath() === '/health' ); -$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(ipResolver: $config->getIpResolver(), cache: $cache))); +$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(cache: $cache))); // ── Blocklists ─────────────────────────────────────────────────────── $config->blocklists->knownScanners(); @@ -733,19 +728,17 @@ $config->fail2ban->add('login-abuse', filter: fn(ServerRequestInterface $req): bool => $req->getMethod() === 'POST' && $req->getUri()->getPath() === '/login', - key: KeyExtractors::clientIp($proxyResolver) ); // ── Rate Limiting ──────────────────────────────────────────────────── $config->throttles->multi('api', [ 5 => 30, // 30 req / 5 sec burst limit 60 => 1000, // 1000 req / min sustained limit -], KeyExtractors::clientIp($proxyResolver)); +]); // ── Allow2Ban ──────────────────────────────────────────────────────── $config->allow2ban->add('flood-protection', threshold: 500, period: 60, banSeconds: 3600, - key: KeyExtractors::clientIp($proxyResolver) ); // ── Application ────────────────────────────────────────────────────── @@ -787,7 +780,6 @@ namespace App\Factory; use Flowd\Phirewall\Config; use Flowd\Phirewall\Config\Rule\SafelistRule; use Flowd\Phirewall\Http\TrustedProxyResolver; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Matchers\TrustedBotMatcher; use Flowd\Phirewall\Middleware as PhirewallMiddleware; use Flowd\Phirewall\Config\Rule\BlocklistRule; @@ -818,16 +810,14 @@ class PhirewallMiddlewareFactory '10.0.0.0/8', '172.16.0.0/12', ]); - $config->setIpResolver( - KeyExtractors::clientIp($proxyResolver) - ); + $config->setIpResolver($proxyResolver->resolve(...)); // ── Safelists ──────────────────────────────────────────── $config->safelists->add('health', fn(ServerRequestInterface $req): bool => $req->getUri()->getPath() === '/health' ); - $config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(ipResolver: $config->getIpResolver(), cache: $cache))); + $config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(cache: $cache))); // ── Blocklists ─────────────────────────────────────────── $config->blocklists->knownScanners(); @@ -950,7 +940,6 @@ The smallest useful configuration. Protects against common scanners and rate-lim ```php use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Store\InMemoryCache; @@ -985,7 +974,6 @@ Tiered per-client-IP rate limits for an API, with a tighter cap on an expensive ```php use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Http\TrustedProxyResolver; use Flowd\Phirewall\Store\RedisCache; use Predis\Client as PredisClient; @@ -995,17 +983,16 @@ $config = new Config(new RedisCache($redis, 'api:')); $config->enableRateLimitHeaders(); $proxyResolver = new TrustedProxyResolver(['10.0.0.0/8', '172.16.0.0/12']); +$config->setIpResolver($proxyResolver->resolve(...)); // Global burst detection $config->throttles->add('burst', limit: 30, period: 5, - key: KeyExtractors::clientIp($proxyResolver) ); // Global per-IP limit $config->throttles->add('global', limit: 1000, period: 60, - key: KeyExtractors::clientIp($proxyResolver) ); // Expensive endpoint limit @@ -1028,7 +1015,6 @@ The sliding window algorithm prevents the "double burst" problem at fixed window ```php use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; $config = new Config(new InMemoryCache()); @@ -1050,7 +1036,6 @@ Apply multiple time windows to a single logical throttle for burst protection al ```php use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; $config = new Config(new InMemoryCache()); @@ -1071,7 +1056,6 @@ Use closures for the `limit` and/or `period` parameters to vary rate limits base ```php use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; use Psr\Http\Message\ServerRequestInterface; @@ -1098,7 +1082,6 @@ Complete login protection with throttling, Fail2Ban, and tracking. ```php use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\RedisCache; use Predis\Client as PredisClient; @@ -1180,7 +1163,6 @@ Use `RequestContext` to signal fail2ban failures after verifying credentials in ```php use Flowd\Phirewall\Config; use Flowd\Phirewall\Context\RequestContext; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; use Psr\Http\Message\ServerRequestInterface; @@ -1271,12 +1253,12 @@ $config = new Config($cache); // Safelist known bots (Googlebot, Bingbot, Baidu, etc.) via RDNS // Pass a PSR-16 cache to avoid repeated DNS lookups -$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(ipResolver: $config->getIpResolver(), cache: $cache))); +$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(cache: $cache))); // Safelist a custom internal bot $config->safelists->addRule(new SafelistRule('custom-bots', new TrustedBotMatcher([ ['ua' => 'mycompany-crawler', 'hostname' => '.crawler.mycompany.com'], -], ipResolver: $config->getIpResolver(), cache: $cache))); +], cache: $cache))); ``` See [Bot Detection](/features/bot-detection) for details. @@ -1331,7 +1313,6 @@ Track rules count requests passively without blocking. Use the optional `limit` ```php use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; $config = new Config(new InMemoryCache()); @@ -1360,7 +1341,6 @@ Use PdoCache with MySQL, PostgreSQL, or SQLite when Redis is not available: ```php use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\PdoCache; // SQLite with file persistence and WAL mode @@ -1434,15 +1414,14 @@ See [PSR-17 Factories](/advanced/psr17) for details. --- -## Production: Comprehensive Multi-Layer Protection +## Production: Multi-Layer Protection -A production-ready configuration combining safelists, blocklists, OWASP rules, bot detection, Fail2Ban, rate limiting, and observability. +A production configuration combining safelists, blocklists, OWASP rules, bot detection, Fail2Ban, rate limiting, and observability. ```php use Flowd\Phirewall\Config; use Flowd\Phirewall\Config\Rule\SafelistRule; use Flowd\Phirewall\Http\TrustedProxyResolver; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Matchers\TrustedBotMatcher; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Config\Rule\BlocklistRule; @@ -1474,7 +1453,7 @@ $proxyResolver = new TrustedProxyResolver([ ]); // Set global IP resolver so all IP-aware matchers use it -$config->setIpResolver(KeyExtractors::clientIp($proxyResolver)); +$config->setIpResolver($proxyResolver->resolve(...)); // === SAFELISTS === $config->safelists->add('health', @@ -1483,7 +1462,7 @@ $config->safelists->add('health', $config->safelists->add('metrics', fn($req) => $req->getUri()->getPath() === '/metrics' ); -$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(ipResolver: $config->getIpResolver(), cache: $cache))); +$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(cache: $cache))); // === BLOCKLISTS === @@ -1524,30 +1503,25 @@ $config->fail2ban->add('login-abuse', threshold: 5, period: 300, ban: 3600, filter: fn($req) => $req->getMethod() === 'POST' && $req->getUri()->getPath() === '/login', - key: KeyExtractors::clientIp($proxyResolver) ); $config->fail2ban->add('persistent-scanner', threshold: 10, period: 60, ban: 86400, filter: fn($req) => true, - key: KeyExtractors::clientIp($proxyResolver) ); // === ALLOW2BAN === $config->allow2ban->add('flood-protection', threshold: 500, period: 60, banSeconds: 3600, - key: KeyExtractors::clientIp($proxyResolver) ); // === THROTTLES === $config->throttles->add('global', limit: 1000, period: 60, - key: KeyExtractors::clientIp($proxyResolver) ); $config->throttles->add('burst', limit: 50, period: 5, - key: KeyExtractors::clientIp($proxyResolver) ); $config->throttles->add('write-ops', @@ -1660,7 +1634,6 @@ Full logging setup with different severity levels: ```php use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\RedisCache; use Flowd\Phirewall\Events\BlocklistMatched; use Flowd\Phirewall\Events\ThrottleExceeded; @@ -1725,7 +1698,6 @@ Complete bot defense with threat feeds and file-backed blocklists: ```php use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\RedisCache; use Predis\Client as PredisClient; diff --git a/docs/faq.md b/docs/faq.md index af4d27e..cb8d053 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -77,7 +77,6 @@ When your application sits behind a load balancer, CDN (Content Delivery Network ```php use Flowd\Phirewall\Http\TrustedProxyResolver; -use Flowd\Phirewall\KeyExtractors; $proxy = new TrustedProxyResolver([ '10.0.0.0/8', // Internal network @@ -85,16 +84,15 @@ $proxy = new TrustedProxyResolver([ '192.168.0.0/16', // Private ranges ]); -// Default client-IP resolution for every rule added without an explicit key -$config->setIpResolver(KeyExtractors::clientIp($proxy)); +// Configure proxy trust once; every rule without an explicit key keys on the resolved client IP. +$config->setIpResolver($proxy->resolve(...)); ``` -You can also use `clientIp()` on individual rules: +Rules added without an explicit `key` argument automatically key on the resolved client IP - no per-rule wiring is needed: ```php -$config->throttles->add('api', limit: 100, period: 60, - key: KeyExtractors::clientIp($proxy), -); +$config->throttles->add('api', limit: 100, period: 60); +// Keys on the resolved client IP (via the resolver set above). ``` A few details worth knowing: @@ -106,7 +104,7 @@ A few details worth knowing: See [Client IP Behind Proxies](/getting-started#client-ip-behind-proxies) for the full behavior. ::: danger -`KeyExtractors::ip()` reads `REMOTE_ADDR`, which behind a CDN or load balancer is the *proxy's* address, so every client collapses onto one key. Always install a client-IP resolver in that case. And never trust `X-Forwarded-For` without configuring trusted proxies: an attacker can otherwise spoof this header to bypass rate limiting. +The raw `REMOTE_ADDR` peer address is the connecting peer, not the client. Behind a CDN or load balancer that is the proxy's address, so every client collapses onto one key. If you need the raw peer address explicitly, read `$request->getServerParams()['REMOTE_ADDR']` directly. For the actual client IP, configure proxy trust once with `$config->setIpResolver((new TrustedProxyResolver([...]))->resolve(...))` and omit the `key` argument on your rules. And never trust `X-Forwarded-For` without configuring trusted proxies: an attacker can otherwise spoof this header to bypass rate limiting. ::: ### What happens when the cache backend is unavailable? @@ -270,7 +268,7 @@ See [Storage Backends](/features/storage) for a detailed comparison. ### Can I use Symfony Cache or Laravel Cache? -Yes, Phirewall accepts any PSR-16 (PHP Standard Recommendation for Simple Caching) compatible implementation, but note that most host-framework caches are **not** PSR-16 out of the box: Laravel's `Cache` repository (`Illuminate\Contracts\Cache\Repository`) and TYPO3's caching framework (`FrontendInterface`) are not `Psr\SimpleCache\CacheInterface`, so passing one directly to `Config` is a type error. Symfony Cache exposes a PSR-16 adapter (`Symfony\Component\Cache\Psr16Cache`) you can wrap a pool in. For production, prefer the bundled `RedisCache` or `ApcuCache`: they implement `CounterStoreInterface` for atomic increments, whereas a generic PSR-16 cache may have non-atomic counter increments. +Yes, Phirewall accepts any PSR-16 (PHP Standard Recommendation for Simple Caching) compatible implementation, but most host-framework caches are **not** PSR-16 out of the box: Laravel's `Cache` repository (`Illuminate\Contracts\Cache\Repository`) and TYPO3's caching framework (`FrontendInterface`) are not `Psr\SimpleCache\CacheInterface`, so passing one directly to `Config` is a type error. Symfony Cache exposes a PSR-16 adapter (`Symfony\Component\Cache\Psr16Cache`) you can wrap a pool in. For production, prefer the bundled `RedisCache` or `ApcuCache`: they implement `CounterStoreInterface` for atomic increments, whereas a generic PSR-16 cache may have non-atomic counter increments. ### Why does InMemoryCache not work in production? @@ -290,7 +288,7 @@ Solutions: No. Phirewall supports a practical subset of the OWASP (Open Web Application Security Project) CRS (Core Rule Set) syntax, covering the most common variables (`ARGS`, `REQUEST_URI`, `REQUEST_HEADERS`, etc.) and operators (`@rx`, `@pm`, `@pmFromFile`, `@contains`, etc.). It is not a full ModSecurity replacement. -For comprehensive OWASP CRS coverage, use a dedicated WAF (like ModSecurity) alongside Phirewall. +For full OWASP CRS coverage, use a dedicated WAF (like ModSecurity) alongside Phirewall. ### How do I load custom OWASP rules? diff --git a/docs/features/bad-ip-preset.md b/docs/features/bad-ip-preset.md index 149e3d0..ef78a88 100644 --- a/docs/features/bad-ip-preset.md +++ b/docs/features/bad-ip-preset.md @@ -42,8 +42,10 @@ bin/badip-import --level=4 ## Limits -- **The blocklist keys on `REMOTE_ADDR`.** Behind a proxy or CDN, configure a trusted client-IP - resolver on the `Config`, or it sees the proxy instead of the client. +- **The blocklist matches the resolved client IP** (the `Config`'s IP resolver, falling back to + `REMOTE_ADDR` when none is set). Behind a proxy or CDN, set the resolver once with + `$config->setIpResolver((new TrustedProxyResolver([...]))->resolve(...))` so it sees the real + client, not the proxy. - **A bundled snapshot goes stale** between refreshes, and a shared host or CGNAT address can be listed for one offender. Prefer a higher level, try `track()` first, and combine with your own allowlist by overriding the rule by name. diff --git a/docs/features/bot-detection.md b/docs/features/bot-detection.md index d74134b..c81b49c 100644 --- a/docs/features/bot-detection.md +++ b/docs/features/bot-detection.md @@ -6,6 +6,10 @@ outline: deep Phirewall provides three specialized matchers for bot and scanner detection: **Known Scanner Blocking**, **Suspicious Headers Detection**, and **Trusted Bot Verification**. Known scanners and suspicious headers are available as one-liner convenience methods on the blocklist section; trusted bot verification is wired by adding a `SafelistRule` with a `TrustedBotMatcher` to the safelist. +::: tip Ready-made bot rules +The companion package [`flowd/phirewall-preset-bots`](/features/bot-presets) ships curated presets that block AI crawlers and rate-limit aggressive SEO bots by User-Agent, complementing the matchers on this page. +::: + ## Known Scanner Blocking The `knownScanners()` method blocks requests whose User-Agent matches known attack tools and vulnerability scanners. It ships with a curated default list covering 24 tools (26 substring patterns, since Burp Suite and Metasploit each have two spellings). @@ -156,7 +160,7 @@ Some legitimate clients may not send all standard headers: API clients, embedded ## Trusted Bot Verification (rDNS) -Wiring a `TrustedBotMatcher` on the safelist safelists verified search engine bots using **reverse DNS (rDNS) verification**. This prevents fake bots: anyone can send `Googlebot` as a User-Agent, but only Google's real crawlers have IPs that resolve to `*.googlebot.com`. +Wiring a `TrustedBotMatcher` onto the safelist verifies search-engine bots using **reverse DNS (rDNS) verification**. This prevents fake bots: anyone can send `Googlebot` as a User-Agent, but only Google's real crawlers have IPs that resolve to `*.googlebot.com`. For rate-limiting verified bots instead of fully safelisting them, see the dedicated [Trusted Bots](/features/trusted-bots) page. @@ -167,13 +171,10 @@ use Flowd\Phirewall\Config\Rule\SafelistRule; use Flowd\Phirewall\Matchers\TrustedBotMatcher; // Safelist verified search engine bots -$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher( - ipResolver: $config->getIpResolver(), - cache: $cache, -))); +$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(cache: $cache))); ``` -Pass `ipResolver: $config->getIpResolver()` so verification uses the correct client IP behind a proxy. Omit it only if you deliberately want to verify against `REMOTE_ADDR`. +By default the matcher uses the `Config`'s IP resolver, so verification picks up the correct client IP behind a proxy with no extra wiring. Pass an explicit `ipResolver:` only to override the resolver for this rule - an explicit resolver is fixed and will not follow a composed `Config`'s merged resolver. ### Configuration @@ -182,16 +183,23 @@ The matcher accepts these constructor arguments: ```php new TrustedBotMatcher( array $additionalBots = [], + ?callable $reverseResolve = null, // test-only DNS seam; do not pass positionally + ?callable $forwardResolve = null, // test-only DNS seam; do not pass positionally ?callable $ipResolver = null, - ?CacheInterface $cache = null + ?CacheInterface $cache = null, + int $cacheTtl = 86400, ) ``` +Because `$ipResolver` is the **4th** parameter, always wire this matcher with **named arguments** (e.g. `new TrustedBotMatcher(cache: $cache)`), never positionally. + | Parameter | Type | Description | |-----------|------|-------------| | `$additionalBots` | `list` | Extra bots to recognize | -| `$ipResolver` | `callable\|null` | IP resolver. Pass `$config->getIpResolver()` to use the config's global resolver (correct client IP behind a proxy). | +| `$reverseResolve` / `$forwardResolve` | `callable\|null` | DNS-lookup overrides, intended as testing seams. Use named arguments if you ever need them; never pass positionally. | +| `$ipResolver` | `callable\|null` | Per-rule IP resolver override. Leave `null` (default) to autowire the `Config`'s resolver - correct client IP behind a proxy. Set it only to resolve this rule's client IP differently from the rest. | | `$cache` | `CacheInterface\|null` | PSR-16 cache for DNS results (highly recommended) | +| `$cacheTtl` | `positive-int` | TTL (seconds) for positive (verified) results. Default `86400` (24h); a value `<= 0` throws `InvalidArgumentException`. The negative-result TTL is the fixed 300s `NEGATIVE_CACHE_TTL`. | ### Verification Flow @@ -239,7 +247,7 @@ Add your organization's internal crawlers: $config->safelists->addRule(new SafelistRule('custom-bots', new TrustedBotMatcher([ ['ua' => 'mycompany-crawler', 'hostname' => '.crawler.mycompany.com'], ['ua' => 'internal-monitor', 'hostname' => '.monitoring.mycompany.com'], -], ipResolver: $config->getIpResolver(), cache: $cache))); +], cache: $cache))); ``` ::: danger @@ -251,10 +259,7 @@ The hostname suffix **must** start with a dot (e.g., `.googlebot.com`, not `goog DNS lookups are blocking I/O operations. **Always provide a PSR-16 cache in production** to avoid latency: ```php -$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher( - ipResolver: $config->getIpResolver(), - cache: $cache, -))); +$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(cache: $cache))); ``` | Cache Behavior | TTL | @@ -301,23 +306,19 @@ $config->blocklists->add('scanner-paths', function ($req): bool { ``` ::: tip -For more comprehensive attack pattern detection beyond path matching, consider using the [OWASP Core Rule Set](/features/owasp-crs) integration which detects SQL injection, XSS, and other attacks in request payloads. +For attack pattern detection beyond path matching, consider the [OWASP Core Rule Set](/features/owasp-crs) integration, which detects SQL injection, XSS, and other attacks in request payloads. ::: ## Combining All Three -Use all three matchers together for comprehensive bot management: +Use all three matchers together for layered bot management: ```php use Flowd\Phirewall\Config\Rule\SafelistRule; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Matchers\TrustedBotMatcher; // 1. Safelist verified search engine bots (they bypass all other rules) -$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher( - ipResolver: $config->getIpResolver(), - cache: $cache, -))); +$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(cache: $cache))); // 2. Block known attack tools $config->blocklists->knownScanners(); diff --git a/docs/features/bot-presets.md b/docs/features/bot-presets.md index e81637d..3bfbdda 100644 --- a/docs/features/bot-presets.md +++ b/docs/features/bot-presets.md @@ -43,6 +43,11 @@ are deliberately excluded, as are robots.txt-only opt-out tokens like `Google-Ex send a truthful `User-Agent`; a hostile scraper can send anything. Use the [OWASP CRS](/features/owasp-crs) and [rate limiting](/features/rate-limiting) presets for hostile traffic. -- **Throttles key on `REMOTE_ADDR`.** Behind a proxy or CDN, configure a trusted client-IP - resolver on the `Config` or every client buckets together. +- **Throttles key on the resolved client IP.** These presets use `PortableConfig::keyIp()`, which late-binds + to the evaluating `Config`'s IP resolver (falling back to `REMOTE_ADDR` when none is set). Behind a proxy or CDN, + set the resolver once on your config and every preset throttle keys on the correct client IP automatically: + ```php + $config->setIpResolver((new \Flowd\Phirewall\Http\TrustedProxyResolver(['10.0.0.1']))->resolve(...)); + ``` + Without a configured resolver, all clients behind the proxy bucket together on the proxy address. - The catalogue is opinionated; override a rule by name to keep a crawler you value. diff --git a/docs/features/fail2ban.md b/docs/features/fail2ban.md index f269483..685b045 100644 --- a/docs/features/fail2ban.md +++ b/docs/features/fail2ban.md @@ -59,7 +59,7 @@ $config->fail2ban->add( | `$period` | `int` | Time window for counting matches in seconds (must be >= 1) | | `$ban` | `int` | Ban duration in seconds (must be >= 1) | | `$filter` | `Closure` | `fn(ServerRequestInterface): bool`, return `true` to count as a match | -| `$key` | `?Closure` | `fn(ServerRequestInterface): ?string`, return key to track, or `null` to skip. When the whole argument is omitted, defaults to the client IP from the Config's IP resolver (`Config::setIpResolver()`, typically `KeyExtractors::clientIp($proxy)`), falling back to `KeyExtractors::ip()` (REMOTE_ADDR). The resolver is read per request, so it can be set before or after the rule. | +| `$key` | `?Closure` | `fn(ServerRequestInterface): ?string`, return key to track, or `null` to skip. When the whole argument is omitted, defaults to the client IP from the Config's IP resolver (`Config::setIpResolver()`), falling back to REMOTE_ADDR. Configure proxy trust once with `$config->setIpResolver((new TrustedProxyResolver([...]))->resolve(...))` and all keyless rules key on the resolved client IP. The resolver is read per request, so it can be set before or after the rule. | ::: warning Fail2Ban filters evaluate the **incoming request** before the handler runs. The filter can only inspect request data (path, method, headers, query parameters). It cannot see the application's response. To ban based on application outcomes (like actual failed logins), use the [Request Context API](#post-handler-signaling-with-requestcontext) instead. @@ -70,7 +70,6 @@ Fail2Ban filters evaluate the **incoming request** before the handler runs. The The most common use case: ban IPs that repeatedly POST to the login endpoint. ```php -use Flowd\Phirewall\KeyExtractors; // Ban after 5 login attempts in 5 minutes, for 1 hour $config->fail2ban->add('login-brute-force', @@ -91,7 +90,6 @@ Counting every POST to `/login` is simpler and works well for most applications. Credential stuffing uses stolen username/password lists from data breaches. Defend against it by combining IP-based banning with user-based throttling: ```php -use Flowd\Phirewall\KeyExtractors; // Per-IP tracking: ban after 10 login attempts in 10 minutes $config->fail2ban->add('credential-stuffing-ip', @@ -157,7 +155,6 @@ $config->fail2ban->add('api-abuse', Ban IPs that persistently probe your application: ```php -use Flowd\Phirewall\KeyExtractors; $config->fail2ban->add('persistent-scanner', threshold: 10, // 10 matched requests @@ -208,7 +205,6 @@ Configure a fail2ban rule with a filter that always returns `false`. The filter ```php use Flowd\Phirewall\Config; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Store\InMemoryCache; use Psr\Http\Message\ServerRequestInterface; @@ -234,8 +230,9 @@ class LoginController { public function handle(ServerRequestInterface $request): ResponseInterface { - $username = $request->getParsedBody()['username'] ?? ''; - $password = $request->getParsedBody()['password'] ?? ''; + $body = (array) $request->getParsedBody(); + $username = $body['username'] ?? ''; + $password = $body['password'] ?? ''; if (!$this->auth->verify($username, $password)) { // Signal the failure; the firewall extracts the key from @@ -273,7 +270,7 @@ Use the null-safe operator (`$context?->recordFailure(...)`) so your handler wor | Approach | Pros | Cons | |----------|------|------| -| **Pre-handler filter** (path/method) | Simple, no handler changes | Counts all attempts, not just failures | +| **Pre-handler filter** (path/method) | Simple, no handler changes | Counts all attempts, not only failures | | **Prior middleware + header** | Can signal actual failures | Requires extra middleware, complex flow | | **RequestContext API** | Signals actual failures from handler | Requires handler integration | @@ -333,7 +330,6 @@ Note the parameter name difference: Fail2Ban uses `$ban`, Allow2Ban uses `$banSe Ban any IP that sends an excessive number of requests: ```php -use Flowd\Phirewall\KeyExtractors; // Ban any IP that sends more than 100 requests in 60 seconds, for 1 hour $config->allow2ban->add( @@ -359,7 +355,7 @@ $config->allow2ban->add( ``` ::: warning Header keys are client-controlled -A throttle, fail2ban, or allow2ban rule keyed on a request header (`X-Api-Key`, `X-User-Id`, …) is only as trustworthy as that header. A client can rotate or drop the header to land in a fresh counter on every request and never reach the threshold (a trivial bypass). Key such rules on a value the client cannot freely change: the client IP (via `KeyExtractors::clientIp()` with a `TrustedProxyResolver`), the authenticated principal your auth layer sets *after* verifying it, or a composite of both. When you must key on a credential-bearing header, use `KeyExtractors::hashedHeader('X-Api-Key')`: the raw value otherwise reaches the ban registry and event payloads (and your logs) in cleartext. +A throttle, fail2ban, or allow2ban rule keyed on a request header (`X-Api-Key`, `X-User-Id`, …) is only as trustworthy as that header. A client can rotate or drop the header to land in a fresh counter on every request and never reach the threshold (a trivial bypass). Key such rules on a value the client cannot freely change: the client IP (configure proxy trust once with `$config->setIpResolver((new TrustedProxyResolver([...]))->resolve(...))` and omit the key so the rule keys on the resolved client IP), the authenticated principal your auth layer sets *after* verifying it, or a composite of both. When you must key on a credential-bearing header, use `KeyExtractors::hashedHeader('X-Api-Key')`: the raw value otherwise reaches the ban registry and event payloads (and your logs) in cleartext. ::: ### Unauthenticated Endpoint Abuse @@ -367,7 +363,6 @@ A throttle, fail2ban, or allow2ban rule keyed on a request header (`X-Api-Key`, Ban clients that repeatedly access authenticated endpoints without credentials: ```php -use Flowd\Phirewall\KeyExtractors; // Ban IPs making more than 20 unauthenticated API requests in 5 minutes $config->allow2ban->add( @@ -520,7 +515,6 @@ Use events to: Fail2Ban and Allow2Ban work best as part of a layered defense: ```php -use Flowd\Phirewall\KeyExtractors; // Layer 1: Safelist trusted traffic $config->safelists->add('health', fn($req) => $req->getUri()->getPath() === '/health'); @@ -558,8 +552,8 @@ $config->throttles->add('global', 5. **Monitor with events.** Always set up logging or alerting for `Fail2BanBanned` and `Allow2BanBanned` events so you know when bans are occurring and can detect false positives. -6. **Use RequestContext for accuracy.** When you need to ban based on actual application failures (not just request patterns), use the [RequestContext API](#post-handler-signaling-with-requestcontext) to signal failures from your handler. +6. **Use RequestContext for accuracy.** When you need to ban based on actual application failures (not only request patterns), use the [RequestContext API](#post-handler-signaling-with-requestcontext) to signal failures from your handler. 7. **Use infrastructure mirroring.** For the most effective defense, mirror bans to Apache `.htaccess` or your web server so banned IPs are blocked before reaching PHP. See [Infrastructure Adapters](/advanced/infrastructure). -8. **Choose the right mechanism.** Use Fail2Ban when you need a filter to detect specific bad behavior. Use Allow2Ban when you want a blanket volume limit with a ban (not just rate limiting). +8. **Choose the right mechanism.** Use Fail2Ban when you need a filter to detect specific bad behavior. Use Allow2Ban when you want a blanket volume limit with a ban (not only rate limiting). diff --git a/docs/features/owasp-crs.md b/docs/features/owasp-crs.md index e31e30c..c0c02a0 100644 --- a/docs/features/owasp-crs.md +++ b/docs/features/owasp-crs.md @@ -145,11 +145,15 @@ $skipped = $report['skipped']; // int - Lines that were skipped | Method | Parameters | Description | |--------|-----------|-------------| -| `fromString()` | `string $rulesText, ?string $contextFolder` | Parse rules from a string | -| `fromFile()` | `string $filePath` | Load rules from a single file | -| `fromFiles()` | `list $paths` | Load and merge multiple files | -| `fromDirectory()` | `string $dir, ?callable $filter` | Load all files in a directory | -| `fromStringWithReport()` | `string $rulesText` | Parse with statistics | +| `fromString()` | `string $rulesText, ?string $contextFolder = null, ?int $maxValuesPerCrsVariable = null` | Parse rules from a string | +| `fromFile()` | `string $filePath, ?int $maxValuesPerCrsVariable = null` | Load rules from a single file | +| `fromFiles()` | `list $paths, ?int $maxValuesPerCrsVariable = null` | Load and merge multiple files | +| `fromDirectory()` | `string $dir, ?callable $filter = null, ?int $maxValuesPerCrsVariable = null` | Load all files in a directory | +| `fromStringWithReport()` | `string $rulesText, ?int $maxValuesPerCrsVariable = null` | Parse with statistics | + +### Per-Variable Value Cap + +Every factory accepts an optional `$maxValuesPerCrsVariable`: a positive-int cap on how many values are collected per CRS variable per request. It bounds the evaluation cost of count-unbounded, attacker-controlled variables such as `ARGS` (a CPU-DoS guard). The default (`null`) derives the cap from twice PHP's `max_input_vars`, falling back to 2000 when the directive is unset or non-positive, so a request PHP can fully parse is never falsely truncated. When a variable *is* truncated at the cap, rules targeting it fail closed and treat the request as a match, so padding a payload past the cap cannot evade a rule. A value `< 1` throws `InvalidArgumentException`. ## Supported SecRule Syntax @@ -190,7 +194,7 @@ Phirewall supports a subset of the ModSecurity SecRule language: | `id:N` | Rule ID (required, must be unique) | | `phase:N` | Processing phase (currently informational) | | `deny` | Block the request (required for the rule to trigger blocking) | -| `block` | Alias for `deny` -- both trigger blocking | +| `block` | Alias for `deny` - both trigger blocking | | `msg:'text'` | Human-readable description for logging | ### Line Continuation @@ -213,6 +217,26 @@ SecRule ARGS "@rx (?i)\bunion\b.*\bselect\b" "id:942100,phase:2,deny,msg:'SQLi'" ## Managing Rules +### Tuning the Bundled Snapshot + +The presets from the [Quick Start](#quick-start) are fixed rule bundles. To tune the bundled +CRS snapshot (for example, to drop a false-positive-prone rule), load it as a mutable +`CoreRuleSet` via `Presets::coreRuleSet()` and wire it yourself: + +```php +use Flowd\PhirewallPresetOwaspCrs\Engine\CoreRuleSetMatcher; +use Flowd\PhirewallPresetOwaspCrs\ParanoiaLevel; +use Flowd\PhirewallPresetOwaspCrs\Presets; + +$rules = Presets::coreRuleSet(ParanoiaLevel::Level2); +$rules->disable(942100); // SQLi via libinjection, if it false-positives for your app + +$config->blocklists->addRule(new BlocklistRule('owasp', new CoreRuleSetMatcher($rules))); +``` + +`Presets::crsVersion()` returns the upstream CRS release tag the bundled rules were +imported from, so you can log or alert on the snapshot your deployment is running. + ### Disabling Rules Disable specific rules that cause false positives: @@ -321,7 +345,7 @@ SecRule REQUEST_URI "@rx (?i)(%2e%2e%2f|%2e%2e/)" \ ## Production Configuration -A comprehensive rule set for production: +A production rule set covering the main attack categories: ```php use Flowd\Phirewall\Config; @@ -445,7 +469,7 @@ Each CRS operator maps to an `OperatorEvaluatorInterface` implementation: | Operator | Evaluator Class | Behavior | |----------|----------------|----------| -| `@rx` | `RegexEvaluator` | PCRE match with auto-delimiters and Unicode mode; values exceeding 8 KiB are skipped (not matched) | +| `@rx` | `RegexEvaluator` | PCRE match with auto-delimiters and Unicode mode; values longer than 8 KiB are truncated to 8,192 bytes and the head is still matched (a PCRE engine error fails closed to a match) | | `@contains` | `ContainsEvaluator` | Case-insensitive substring search | | `@streq` | `StringEqualEvaluator` | Case-insensitive exact match | | `@startswith` / `@beginswith` | `StartsWithEvaluator` | Case-insensitive prefix match | @@ -456,9 +480,9 @@ Each CRS operator maps to an `OperatorEvaluatorInterface` implementation: Unsupported operators resolve to `UnsupportedOperatorEvaluator`, which never matches (safe no-op). ::: warning ReDoS protection: 8 KiB length guard on `@rx` -`RegexEvaluator` skips any value whose byte length exceeds 8,192 bytes — the value is treated as non-matching. This is an intentional trade-off: running PCRE on unbounded attacker-controlled input risks catastrophic backtracking that can freeze the PHP process (ReDoS). Skipping overlength values mirrors the behavior of standard WAFs such as ModSecurity's `SecRequestBodyLimit`. +`RegexEvaluator` does **not** skip overlength values. A value longer than 8,192 bytes is truncated to that length (dropping a partial trailing UTF-8 sequence) and the **head is still matched** against the pattern. This bounds the PCRE work on unbounded attacker-controlled input - which risks catastrophic backtracking that can freeze the PHP process (ReDoS) - while preventing evasion by padding a payload past the limit. A value that triggers a PCRE engine error (catastrophic backtracking, invalid UTF-8 under `/u`, backtrack/recursion limit) is treated as a **match** (fail-closed), so a malformed payload can never silently disable a rule. -In practice, legitimate request values (query parameters, header values, cookie values) are rarely larger than a few kilobytes. If you are matching multi-megabyte request bodies via `@rx`, consider pre-processing them before passing to the firewall. +In practice, legitimate request values (query parameters, header values, cookie values) are rarely larger than a few kilobytes, so the truncation only affects oversized, likely-hostile input. ::: ### Adding Custom Operators @@ -545,7 +569,6 @@ Use `@pm` for simple keyword matching and `@rx` for complex patterns. `@pm` is s $config->fail2ban->add('persistent-attacker', threshold: 5, period: 60, ban: 86400, filter: fn($req) => true, - key: KeyExtractors::ip() ); ``` diff --git a/docs/features/rate-limiting.md b/docs/features/rate-limiting.md index 27129e6..4da4587 100644 --- a/docs/features/rate-limiting.md +++ b/docs/features/rate-limiting.md @@ -15,7 +15,7 @@ Three throttle strategies are available: | **Multi-window** | `multi()` | Combined burst + sustained limits | ::: tip Default key -The `key` argument on `add()`, `sliding()`, and `multi()` is optional. When omitted, the throttle keys on the client IP resolved by the Config's IP resolver (set via `Config::setIpResolver(KeyExtractors::clientIp($trustedProxyResolver))` behind a proxy), falling back to `KeyExtractors::ip()` (REMOTE_ADDR) when none is set. The resolver is read per request, so it can be set before or after adding rules. The examples below omit `key:` to use this default; pass an explicit `key:` only to key on something other than the client IP (a header, a username, and so on). +The `key` argument on `add()`, `sliding()`, and `multi()` is optional. When omitted, the throttle keys on the client IP resolved by the Config's IP resolver (set via `$config->setIpResolver((new TrustedProxyResolver([...]))->resolve(...))` behind a proxy), falling back to REMOTE_ADDR when none is set. The resolver is read per request, so it can be set before or after adding rules. The examples below omit `key:` to use this default; pass an explicit `key:` only to key on something other than the client IP (a header, a username, and so on). ::: ## Fixed Window Throttle @@ -39,8 +39,6 @@ $config->throttles->add( | `$key` | `?Closure` | `fn(ServerRequestInterface): ?string`, return a key to group by, or `null` to skip. Omit to default to the client IP (Config IP resolver, else REMOTE_ADDR). | ```php -use Flowd\Phirewall\KeyExtractors; - // 100 requests per minute per IP $config->throttles->add('ip-limit', limit: 100, period: 60); ``` @@ -218,14 +216,16 @@ Phirewall ships with common key extractors for typical rate limiting scenarios: | Helper | Description | Returns | |--------|-------------|---------| -| `KeyExtractors::ip()` | Client IP from `REMOTE_ADDR` | `?string` | -| `KeyExtractors::clientIp($resolver)` | Client IP via trusted proxy resolver | `?string` | +| `KeyExtractors::ip()` | **Deprecated.** Keyed on raw `REMOTE_ADDR`. Omit the key to key on the resolved client IP instead; read `$request->getServerParams()['REMOTE_ADDR']` directly if you genuinely need the raw peer. | `?string` | +| `KeyExtractors::clientIp($resolver)` | **Deprecated.** Was the per-rule proxy-aware key. Set the resolver once with `$config->setIpResolver($resolver->resolve(...))` and omit the key; keyless rules then key on the resolved client IP. | `?string` | | `KeyExtractors::header('X-User-Id')` | Raw value of a specific header | `?string` | | `KeyExtractors::hashedHeader('X-Api-Key')` | sha256 fingerprint of a header value; preferred for credential-bearing headers (raw value never stored/emitted) | `?string` | | `KeyExtractors::method()` | HTTP method (uppercase) | `?string` | | `KeyExtractors::path()` | Request path (always returns a value, never skips) | `string` | | `KeyExtractors::userAgent()` | User-Agent header value | `?string` | +The client IP is the default key when `key:` is omitted - no extractor needed. Set proxy trust once with `$config->setIpResolver((new TrustedProxyResolver([...]))->resolve(...))` and all keyless rules key on the resolved client IP automatically. + All extractors except `path()` return `null` when the value is missing or empty, which causes the throttle rule to be skipped for that request. Prefer `hashedHeader()` over `header()` whenever the header carries a credential (`Authorization`, `Cookie`, `X-Api-Key`, …). The cache backend and ban registry then store the sha256 fingerprint rather than the raw secret, so anyone with read access to the cache cannot recover the credential. @@ -260,18 +260,17 @@ Define multiple throttle rules with different limits for different use cases. Al ```php use Flowd\Phirewall\Http\TrustedProxyResolver; -use Flowd\Phirewall\KeyExtractors; $proxyResolver = new TrustedProxyResolver([ '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', ]); +$config->setIpResolver($proxyResolver->resolve(...)); -// Tier 1: Global per-IP limit +// Tier 1: Global per-IP limit (keyless - keys on the resolved client IP) $config->throttles->add('global-ip', limit: 1000, period: 60, - key: KeyExtractors::clientIp($proxyResolver) ); // Tier 2: Stricter limit for write operations @@ -299,10 +298,10 @@ $config->throttles->add('search-endpoint', ## Per-User Limits -Enforce rate limits at the firewall on the client IP, which a caller cannot forge (behind a proxy, resolve it with `KeyExtractors::clientIp()` and a `TrustedProxyResolver`). Do not key a limit on a client-supplied header such as `X-User-Id` or `X-Api-Key`: a caller can rotate or drop it to land in a fresh counter on every request and never reach the limit. For genuine per-authenticated-user limits, enforce them behind your application's auth layer, where the user identity has been verified, rather than on a raw request header at the edge. +Enforce rate limits at the firewall on the client IP, which a caller cannot forge (behind a proxy, configure proxy trust once with `$config->setIpResolver((new TrustedProxyResolver([...]))->resolve(...))` and rules key on the resolved client IP by default - omit the key). Do not key a limit on a client-supplied header such as `X-User-Id` or `X-Api-Key`: a caller can rotate or drop it to land in a fresh counter on every request and never reach the limit. For genuine per-authenticated-user limits, enforce them behind your application's auth layer, where the user identity has been verified, rather than on a raw request header at the edge. ::: warning Header keys are client-controlled -A throttle, fail2ban, or allow2ban rule keyed on a request header (`X-Api-Key`, `X-User-Id`, …) is only as trustworthy as that header. A client can rotate or drop the header to land in a fresh counter on every request and never reach the threshold (a trivial bypass). Key such rules on a value the client cannot freely change: the client IP (via `KeyExtractors::clientIp()` with a `TrustedProxyResolver`), the authenticated principal your auth layer sets *after* verifying it, or a composite of both. When you must key on a credential-bearing header, use `KeyExtractors::hashedHeader('X-Api-Key')`: the raw value otherwise reaches the ban registry and event payloads (and your logs) in cleartext. +A throttle, fail2ban, or allow2ban rule keyed on a request header (`X-Api-Key`, `X-User-Id`, …) is only as trustworthy as that header. A client can rotate or drop the header to land in a fresh counter on every request and never reach the threshold (a trivial bypass). Key such rules on a value the client cannot freely change: the client IP (set proxy trust with `$config->setIpResolver((new TrustedProxyResolver([...]))->resolve(...))` and omit the key so the rule keys on the resolved client IP), the authenticated principal your auth layer sets *after* verifying it, or a composite of both. When you must key on a credential-bearing header, use `KeyExtractors::hashedHeader('X-Api-Key')`: the raw value otherwise reaches the ban registry and event payloads (and your logs) in cleartext. ::: ## Rate Limit Headers @@ -369,7 +368,6 @@ When your application sits behind a load balancer, CDN, or reverse proxy, `REMOT ```php use Flowd\Phirewall\Http\TrustedProxyResolver; -use Flowd\Phirewall\KeyExtractors; $resolver = new TrustedProxyResolver([ '10.0.0.0/8', // Internal network @@ -377,22 +375,18 @@ $resolver = new TrustedProxyResolver([ '192.168.0.0/16', // Private ranges '2001:db8::/32', // IPv6 support ]); +$config->setIpResolver($resolver->resolve(...)); -$config->throttles->add('api', limit: 100, period: 60, - key: KeyExtractors::clientIp($resolver) -); +// All keyless rules now key on the resolved client IP +$config->throttles->add('api', limit: 100, period: 60); ``` -You can also set a global IP resolver so all IP-aware matchers use it automatically: - -```php -$config->setIpResolver(KeyExtractors::clientIp($resolver)); -``` +Setting the IP resolver once on the Config is all that is needed. All IP-aware matchers (throttles, fail2ban, allow2ban, filterIp, keyIp) then resolve the client IP consistently through the same resolver. The resolver's `allowedHeaders` argument defaults to `['X-Forwarded-For']` (a single header); pass `['Forwarded']` explicitly if your stack emits the RFC 7239 header. All forwarded-header instances are folded into one chain and walked right to left, returning the first hop not in your trusted-proxy list (so the trusted-proxy ranges, not the number of header lines, are what prevent spoofing), and IPv6 addresses are canonicalized (IPv4-mapped peers match IPv4 rules). See [Client IP Behind Proxies](/getting-started#client-ip-behind-proxies) for the full behavior. ::: danger -`KeyExtractors::ip()` keys on raw `REMOTE_ADDR`; behind a load balancer or CDN that is the proxy IP, so every client shares one throttle key and your limits stop working. Configure a `TrustedProxyResolver` so rate limits apply to the real client. And never trust `X-Forwarded-For` without configuring trusted proxies: an attacker can otherwise spoof this header to bypass rate limiting entirely. +The raw `REMOTE_ADDR` peer address is the proxy IP behind a load balancer or CDN, so every client would share one throttle key and your limits would stop working. Configure a `TrustedProxyResolver` so rate limits apply to the real client. And never trust `X-Forwarded-For` without configuring trusted proxies: an attacker can otherwise spoof this header to bypass rate limiting entirely. ::: ## Events @@ -422,7 +416,7 @@ Use this event for alerting, logging, or triggering further actions. See [Observ 3. **Use dynamic limits for per-plan pricing.** A single rule with a closure is cleaner than separate rules per subscription tier. -4. **Use `clientIp()` in production.** Raw `REMOTE_ADDR` is the proxy IP behind load balancers. Always configure trusted proxies. +4. **Configure `setIpResolver()` in production.** Raw `REMOTE_ADDR` is the proxy IP behind load balancers. Set `$config->setIpResolver((new TrustedProxyResolver([...]))->resolve(...))` once and all keyless rules key on the resolved client IP automatically. 5. **Return `null` to skip.** Key closures that return `null` cause the rule to be skipped entirely for that request, with zero overhead. diff --git a/docs/features/safelists-blocklists.md b/docs/features/safelists-blocklists.md index 5c159d8..40c5cb5 100644 --- a/docs/features/safelists-blocklists.md +++ b/docs/features/safelists-blocklists.md @@ -97,32 +97,19 @@ $config->safelists->ip('ipv6-loopback', '::1'); Safelist verified search engine bots via reverse DNS verification. Wire `TrustedBotMatcher` onto the safelist with `addRule()`. See [Bot Detection](/features/bot-detection) for full details, and [Trusted Bots](/features/trusted-bots) for the matcher itself. -```php -use Flowd\Phirewall\Config\Rule\SafelistRule; -use Flowd\Phirewall\Matchers\TrustedBotMatcher; - -$config->safelists->addRule(new SafelistRule($name, new TrustedBotMatcher( - additionalBots: [], - ipResolver: $config->getIpResolver(), - cache: null, -))); -``` - -Pass `ipResolver: $config->getIpResolver()` so verification uses the real client IP behind a proxy, matching the [global IP resolver](#ip-resolution). +The matcher **autowires the `Config`'s [global IP resolver](#ip-resolution)** when `ipResolver` is omitted, so verification uses the real client IP behind a proxy with no extra wiring. Pass an explicit `ipResolver:` only to override the resolver for a single rule - an explicit resolver is fixed and will not follow a composed `Config`'s merged resolver. ```php use Flowd\Phirewall\Config\Rule\SafelistRule; use Flowd\Phirewall\Matchers\TrustedBotMatcher; // Safelist Google, Bing, Yahoo, Baidu, DuckDuckGo, Yandex, and Apple bots -$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher( - ipResolver: $config->getIpResolver(), -))); +$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher())); // Add custom bots on top of the built-in list $config->safelists->addRule(new SafelistRule('bots', new TrustedBotMatcher([ ['ua' => 'mypartnerbot', 'hostname' => '.partner.example.com'], -], ipResolver: $config->getIpResolver()))); +]))); ``` ::: warning @@ -132,10 +119,7 @@ Without a PSR-16 cache, each request with a bot-like User-Agent triggers blockin use Flowd\Phirewall\Config\Rule\SafelistRule; use Flowd\Phirewall\Matchers\TrustedBotMatcher; -$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher( - ipResolver: $config->getIpResolver(), - cache: $cache, -))); +$config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher(cache: $cache))); ``` ::: @@ -240,7 +224,7 @@ $config->blocklists->knownScanners( The built-in list covers: sqlmap, nikto, nmap, masscan, zmeu, havij, acunetix, nessus, openvas, w3af, dirbuster, gobuster, wfuzz, hydra, medusa, burpsuite, skipfish, whatweb, metasploit, nuclei, ffuf, feroxbuster, joomscan, and wpscan (26 substring patterns in total; Burp Suite and Metasploit are each matched under two spellings). ```php -// Use defaults: blocks 24 known attack tools +// Use defaults: 26 built-in scanner patterns $config->blocklists->knownScanners(); // Add custom patterns on top of defaults @@ -491,6 +475,8 @@ $config->blocklists->patternBlocklist('threat-intel', $entries); ::: tip Pattern backends are also the serializable, database-friendly equivalent of file-backed lists. To keep a block catalogue outside code (in a settings table or config service) and hot-reload it on change, express it as a [Portable Config](/advanced/portable-config). + +For a ready-made known-bad-IP blocklist built from a bundled threat-intelligence feed snapshot, use the companion package [`flowd/phirewall-preset-bad-ips`](/features/bad-ip-preset) instead of wiring your own feed. ::: ## IP Resolution {#ip-resolution} @@ -499,11 +485,10 @@ Both `safelists->ip()` and `blocklists->ip()` respect the global IP resolver set ```php use Flowd\Phirewall\Http\TrustedProxyResolver; -use Flowd\Phirewall\KeyExtractors; // Set a global IP resolver for all IP-aware matchers $proxy = new TrustedProxyResolver(['10.0.0.0/8']); -$config->setIpResolver(KeyExtractors::clientIp($proxy)); +$config->setIpResolver($proxy->resolve(...)); // Now all ip() calls use the real client IP, not the proxy IP $config->safelists->ip('office', '203.0.113.10'); @@ -526,7 +511,7 @@ $config->safelists->ip('cloudflare-office', '203.0.113.10', ipResolver: $customR - **Alternate IPv6 spellings** (expanded `2001:0db8:0:0:0:0:0:1` vs compressed `2001:db8::1`, upper vs lower case) all resolve to one canonical identity, so a rule in any spelling matches all of them. ::: danger -`KeyExtractors::ip()` reads raw `REMOTE_ADDR`; behind a proxy or CDN that is the proxy's address, so IP rules match the proxy rather than the client. Set a client-IP resolver (above) in that case. And never trust `X-Forwarded-For` without configuring trusted proxies; an attacker can otherwise spoof this header to bypass IP-based rules. See [Client IP Behind Proxies](/getting-started#client-ip-behind-proxies). +The raw `REMOTE_ADDR` peer address is the proxy's address behind a proxy or CDN, so IP rules would match the proxy rather than the client. Set a client-IP resolver (above) in that case. If you need to read the raw peer address explicitly, use `$request->getServerParams()['REMOTE_ADDR']` directly. And never trust `X-Forwarded-For` without configuring trusted proxies; an attacker can otherwise spoof this header to bypass IP-based rules. See [Client IP Behind Proxies](/getting-started#client-ip-behind-proxies). ::: ## Evaluation Order diff --git a/docs/features/storage.md b/docs/features/storage.md index 2027733..0be2fba 100644 --- a/docs/features/storage.md +++ b/docs/features/storage.md @@ -80,7 +80,7 @@ Two distinct problems make `InMemoryCache` unsuitable for the firewall under lon 1. **No shared state across workers.** Each worker is a separate OS process with its own array, so a counter or ban set in one worker is invisible to the others. The effective rate limit becomes roughly N times the configured value (N workers), and a client banned on one worker is not banned on the rest. 2. **Coroutine races within a worker.** In coroutine servers like Swoole, the plain PHP arrays have no locking, so concurrent coroutines in the same worker can race. -Use a shared store under these runtimes: `RedisCache` (preferred) or `PdoCache`. Note that `ApcuCache` does **not** solve problem 1: APCu memory is per process, so counters and bans still fragment across workers. +Use a shared store under these runtimes: `RedisCache` (preferred) or `PdoCache`. `ApcuCache` does **not** solve problem 1: APCu memory is per process, so counters and bans still fragment across workers. ::: ## ApcuCache diff --git a/docs/features/trusted-bots.md b/docs/features/trusted-bots.md index 4eee1e9..f31a216 100644 --- a/docs/features/trusted-bots.md +++ b/docs/features/trusted-bots.md @@ -73,8 +73,11 @@ the limit is bucketed: - A constant like `'trusted-bot'` shares one bucket across every verified crawler (a global cap on all trusted bots combined). -- The client IP - `KeyExtractors::ip()($request)` - gives each verified crawler IP its own - cap. +- The client IP gives each verified crawler IP its own cap. The raw `REMOTE_ADDR` peer address + is the connecting peer; behind a proxy or CDN that is the proxy's address. Keep proxy trust + configured in one place: reuse the resolver instance you registered via `setIpResolver()` and + return `$resolver->resolve($request)` from the key closure to bucket on the real client. Read + `$request->getServerParams()['REMOTE_ADDR']` directly if you need the raw peer address. - A per-bot token gives each crawler family a separate cap. ## Custom bots diff --git a/docs/getting-started.md b/docs/getting-started.md index 8b3b129..ae5bc56 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -37,7 +37,7 @@ composer require monolog/monolog ## Step 1: Choose a Storage Backend -Phirewall needs a PSR-16 (PHP Standard Recommendation for Simple Caching) cache for storing counters and ban states. Pick the backend that fits your deployment. If you are just trying Phirewall locally, start with `InMemoryCache`; choose a shared backend (Redis or PDO) before production. +Phirewall needs a PSR-16 (PHP Standard Recommendation for Simple Caching) cache for storing counters and ban states. Pick the backend that fits your deployment. If you are trying Phirewall locally, start with `InMemoryCache`; choose a shared backend (Redis or PDO) before production. ::: code-group @@ -107,14 +107,18 @@ Every safelist, blocklist, throttle, fail2ban, and track callback receives the i Safelisted requests bypass all other rules. Use them for health checks, internal monitoring, and other trusted traffic. ```php +use Flowd\Phirewall\Config\Rule\SafelistRule; +use Flowd\Phirewall\Matchers\TrustedBotMatcher; + $config->safelists->add('health', fn($req) => $req->getUri()->getPath() === '/health'); $config->safelists->add('metrics', fn($req) => $req->getUri()->getPath() === '/metrics'); // Safelist specific IPs or CIDR ranges $config->safelists->ip('office', ['10.0.0.0/8', '192.168.1.0/24']); -// Safelist verified search engine bots (Googlebot, Bingbot, etc.). -// Verified via reverse DNS; pass a cache to skip repeat lookups (see Bot Detection). +// Safelist verified search engine bots (Googlebot, Bingbot, etc.). The matcher uses the +// Config's IP resolver automatically; in production also pass a PSR-16 cache to skip +// repeat reverse-DNS lookups (see Bot Detection). $config->safelists->addRule(new SafelistRule('trusted-bots', new TrustedBotMatcher())); ``` @@ -141,7 +145,7 @@ $config->blocklists->suspiciousHeaders(); ### Throttling (Rate Limiting) -Throttled requests receive `429 Too Many Requests` with a `Retry-After` header. Counting rules (throttle, fail2ban, allow2ban, track) count requests against a *key*, an identity that defaults to the client IP (`KeyExtractors::ip()`, which reads `REMOTE_ADDR`). Pass a `key:` with a `KeyExtractors::*` callable to count against something else; behind a proxy, set the real client IP with a resolver (see [Client IP Behind Proxies](#client-ip-behind-proxies)). +Throttled requests receive `429 Too Many Requests` with a `Retry-After` header. Counting rules (throttle, fail2ban, allow2ban, track) count requests against a *key*, an identity that defaults to the resolved client IP (the Config's IP resolver, falling back to `REMOTE_ADDR` when no resolver is set). Pass a `key:` with a `KeyExtractors::*` callable to count against something else; behind a proxy, configure proxy trust once on the Config with `setIpResolver` (see [Client IP Behind Proxies](#client-ip-behind-proxies)). ```php // 100 requests per minute per IP @@ -303,7 +307,6 @@ namespace App\Factory; use Flowd\Phirewall\Config; use Flowd\Phirewall\Config\Rule\SafelistRule; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Matchers\TrustedBotMatcher; use Flowd\Phirewall\Middleware; use Flowd\Phirewall\Store\ApcuCache; @@ -456,7 +459,6 @@ namespace App\Providers; use Flowd\Phirewall\Config; use Flowd\Phirewall\Config\Rule\SafelistRule; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Matchers\TrustedBotMatcher; use Flowd\Phirewall\Middleware as PhirewallMiddleware; use Flowd\Phirewall\Store\ApcuCache; @@ -599,7 +601,6 @@ final readonly class Phirewall use Flowd\Phirewall\Config; use Flowd\Phirewall\Config\Rule\SafelistRule; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Matchers\TrustedBotMatcher; use Flowd\Phirewall\Middleware as PhirewallMiddleware; use Flowd\Phirewall\Store\ApcuCache; @@ -667,7 +668,6 @@ namespace App\Factory; use Flowd\Phirewall\Config; use Flowd\Phirewall\Config\Rule\SafelistRule; -use Flowd\Phirewall\KeyExtractors; use Flowd\Phirewall\Matchers\TrustedBotMatcher; use Flowd\Phirewall\Middleware as PhirewallMiddleware; use Flowd\Phirewall\Store\ApcuCache; @@ -729,7 +729,7 @@ class PhirewallMiddlewareFactory ::: -> **Middleware ordering:** Pipe Phirewall as early as possible, but after your error-handling middleware. Phirewall does not wrap the downstream handler, so a handler exception must be able to reach the error handler. See the [Examples](/examples#framework-integration) page for more detailed, production-ready integrations. +> **Middleware ordering:** Pipe Phirewall as early as possible, but after your error-handling middleware. Phirewall does not wrap the downstream handler, so a handler exception must be able to reach the error handler. See the [Examples](/examples#framework-integration) page for more detailed integrations. ## Complete Example @@ -866,7 +866,6 @@ When your application sits behind a load balancer or CDN (Content Delivery Netwo ```php use Flowd\Phirewall\Http\TrustedProxyResolver; -use Flowd\Phirewall\KeyExtractors; $resolver = new TrustedProxyResolver([ '10.0.0.0/8', // Internal network @@ -874,17 +873,15 @@ $resolver = new TrustedProxyResolver([ '192.168.0.0/16', // Private ranges ]); -// Use as a key extractor for any rule -$config->throttles->add('api', limit: 100, period: 60, - key: KeyExtractors::clientIp($resolver) -); +// Configure proxy trust once on the Config; all rules then key on +// the resolved client IP by default (no key: argument needed). +$config->setIpResolver($resolver->resolve(...)); -// Or set globally so all IP-aware matchers use it -$config->setIpResolver(KeyExtractors::clientIp($resolver)); +$config->throttles->add('api', limit: 100, period: 60); ``` ::: danger Set a resolver behind a proxy, or every client shares one key -`KeyExtractors::ip()` reads `REMOTE_ADDR` verbatim. Behind a CDN or load balancer that value is the *proxy's* address, so every client collapses onto a single throttle/ban key and your rate limits and bans become useless (or ban everyone at once). The same default applies to file-backed IP blocklists and infrastructure ban listeners. Whenever Phirewall runs behind a proxy, install a client-IP resolver, `$config->setIpResolver(KeyExtractors::clientIp(new TrustedProxyResolver([...])))`, so rules key on the originating client. And never trust `X-Forwarded-For` *without* configuring the trusted proxies: an attacker can otherwise spoof the header to forge any client IP. +The raw `REMOTE_ADDR` peer address is used verbatim when no resolver is configured. Behind a CDN or load balancer that value is the *proxy's* address, so every client collapses onto a single throttle/ban key and your rate limits and bans become useless (or ban everyone at once). The same default applies to file-backed IP blocklists and infrastructure ban listeners. Whenever Phirewall runs behind a proxy, install a client-IP resolver, `$config->setIpResolver((new TrustedProxyResolver([...]))->resolve(...))`, so rules key on the originating client. And never trust `X-Forwarded-For` *without* configuring the trusted proxies: an attacker can otherwise spoof the header to forge any client IP. ::: ### Resolver behavior @@ -901,9 +898,9 @@ new TrustedProxyResolver( ``` - **The default header is a single header.** `allowedHeaders` defaults to `['X-Forwarded-For']` only. If your stack emits the RFC 7239 `Forwarded` header instead, pass it explicitly: `new TrustedProxyResolver([...], ['Forwarded'])`, or `['Forwarded', 'X-Forwarded-For']` for both, so the header the resolver trusts is visible at the call site rather than inferred. -- **Proxy headers are read only when the direct peer is trusted.** The resolver consults `X-Forwarded-For` (or `Forwarded`) only when `REMOTE_ADDR`, the address that actually connected, is itself in your trusted-proxy list. A request arriving directly from an untrusted client has its forwarded headers ignored and is keyed on `REMOTE_ADDR`. +- **Proxy headers are read only when the direct peer is trusted.** The resolver consults `X-Forwarded-For` (or `Forwarded`) only when `REMOTE_ADDR`, the address that connected, is itself in your trusted-proxy list. A request arriving directly from an untrusted client has its forwarded headers ignored and is keyed on `REMOTE_ADDR`. - **All header instances are folded into one chain.** Whether intermediaries keep `X-Forwarded-For` as separate lines or fold them into one comma-separated value (the nginx default), the resolver flattens them and walks the chain right to left, returning the first hop that is not in your trusted-proxy list. The protection is this trusted-hop walk, not the number or order of header instances: a client-prepended value sits to the left of the addresses your proxies append, so it is returned only if every hop to its right is trusted. Correct trusted ranges are therefore essential, and stripping or overwriting the inbound header at the edge prevents spoofing outright. -- **IPv6 is canonicalized.** An IPv4-mapped IPv6 peer (`::ffff:203.0.113.7`) collapses to its embedded IPv4 form, so a plain IPv4 rule or CIDR matches it and an attacker cannot bypass an IPv4 rule by presenting the mapped form. Alternate *genuine*-IPv6 spellings (expanded `2001:0db8::1` vs compressed `2001:db8::1`, mixed case) are also treated as one identity by `ip()` / CIDR **list** matching, which compares the raw binary address. When keys are derived through `KeyExtractors::clientIp()`, the resolver canonicalizes the address it returns, so per-client keys stay stable regardless of the spelling the client presents; the consistent-spelling caveat applies only to raw `KeyExtractors::ip()` (`REMOTE_ADDR`) or a custom resolver that does not canonicalize. +- **IPv6 is canonicalized.** An IPv4-mapped IPv6 peer (`::ffff:203.0.113.7`) collapses to its embedded IPv4 form, so a plain IPv4 rule or CIDR matches it and an attacker cannot bypass an IPv4 rule by presenting the mapped form. Alternate *genuine*-IPv6 spellings (expanded `2001:0db8::1` vs compressed `2001:db8::1`, mixed case) are also treated as one identity by `ip()` / CIDR **list** matching, which compares the raw binary address. When a `TrustedProxyResolver` is set via `setIpResolver`, the resolver canonicalizes the address it returns, so per-client keys stay stable regardless of the spelling the client presents; the consistent-spelling caveat applies only to the raw `REMOTE_ADDR` peer address (read via `$request->getServerParams()['REMOTE_ADDR']`) or a custom resolver that does not canonicalize. ## First Test @@ -925,6 +922,7 @@ curl -i http://localhost:8080/health - Learn about [Safelists & Blocklists](/features/safelists-blocklists) - Configure [Rate Limiting](/features/rate-limiting) - Set up [Fail2Ban & Allow2Ban](/features/fail2ban) for brute force protection +- Add ready-made presets from the companion packages: [OWASP CRS](/features/owasp-crs), [Bot Presets](/features/bot-presets), and the [Bad-IP Preset](/features/bad-ip-preset) - Explore [Storage Backends](/features/storage) for production - Add [Observability](/advanced/observability) for monitoring - Use [Request Context](/advanced/request-context) for post-handler failure signaling diff --git a/docs/index.md b/docs/index.md index 7737ef8..076f7cc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,7 +20,7 @@ features: details: Fixed-window, sliding-window, and multi-window throttling with dynamic per-request limits. - icon: "\uD83D\uDEE1\uFE0F" title: Bot Detection - details: Block known scanners, verify search engine bots via rDNS, and detect suspicious headers, each with a single method call. + details: Block known scanners and detect suspicious headers with a single method call. Verify search engine bots via reverse DNS using the built-in TrustedBotMatcher. - icon: "\uD83D\uDEAB" title: IP Blocking & Safelisting details: Safelist and blocklist IPs and CIDR ranges. Pattern backends with file-backed persistence and automatic expiration. diff --git a/docs/services.md b/docs/services.md index fc1de3f..1c9ec94 100644 --- a/docs/services.md +++ b/docs/services.md @@ -82,7 +82,7 @@ Design and development of the Mankido marketing website on TYPO3, providing a fl ## How We Work - **Long-term partnerships** over one-off projects -- **Direct, technical communication**: no classic project managers, just senior engineers +- **Direct, technical communication**: no classic project managers, only senior engineers - **Transparency & responsibility**: open error culture, clear budgets, accountability in operations ## Get in Touch