# Bad IP Blocker for Laravel Block requests from bad IP addresses using a local JSON list, and automatically block clients that probe for paths no normal visitor requests (`/.well-known/security.txt`, `/.env`, `/wp-login.php`, …). ## Features - Local JSON list – no external service, no database - Automatic blocking of scanners (only when the requested path returns 404 in your app) - Artisan commands and a facade to manage the list - Optional expiry (TTL) per block - Whitelist with CIDR support (private and loopback ranges by default) - Laravel 11, 12 and 13 ## Installation ```bash composer require mattitja/bad-ip-blocker ``` Publish the config (optional): ```bash php artisan vendor:publish --tag=bad-ip-blocker-config ``` ## Usage That's it – after `composer require` the middleware is registered globally and starts blocking. Expired blocks are pruned daily via the Laravel scheduler (make sure `schedule:run` is running). If you'd rather register the middleware yourself, set `BAD_IP_BLOCKER_AUTO_REGISTER=false` and add it in `bootstrap/app.php`: ```php use Mattitja\BadIpBlocker\Middleware\CheckBadIps; ->withMiddleware(function (Middleware $middleware) { $middleware->append(CheckBadIps::class); }) ``` It must run globally (not only in a route group) so it also sees requests for routes that don't exist. Registering it both ways is harmless – it only runs once. ### Behind a proxy or load balancer – important This is the one thing you may need to configure. The middleware uses `$request->ip()`. Behind a reverse proxy, load balancer or Cloudflare this returns the proxy's address unless you trust the proxy. Configure it so the real client IP from `X-Forwarded-For` is used: ```php ->withMiddleware(function (Middleware $middleware) { $middleware->trustProxies(at: ['10.0.0.0/8']); // your proxy addresses }) ``` Private ranges are whitelisted by default, so a missing proxy config never blocks every visitor. But until you configure trusted proxies, **nothing is blocked** behind the proxy either – it fails silently. ## Managing the list ### Artisan ```bash php artisan bad-ip-blocker:add 203.0.113.10 --reason="Scraper" --ttl=86400 php artisan bad-ip-blocker:remove 203.0.113.10 php artisan bad-ip-blocker:list php artisan bad-ip-blocker:prune # remove expired blocks php artisan bad-ip-blocker:clear --force # remove all blocks ``` ### Facade ```php use Mattitja\BadIpBlocker\Facades\BadIpBlocker; BadIpBlocker::block('203.0.113.10', 'Scraper', ttl: 3600); // ttl in seconds, null = permanent BadIpBlocker::unblock('203.0.113.10'); BadIpBlocker::isBlocked('203.0.113.10'); BadIpBlocker::all(); ``` ### Pruning Expired blocks are ignored automatically and removed from the file by a daily scheduled `bad-ip-blocker:prune`. Disable it with `BAD_IP_BLOCKER_SCHEDULE_PRUNE=false`. ## Automatic blocking When a request matches a pattern in `auto_block.paths` **and your app responds with 404**, the IP is blocked. Routes that exist in your app are never affected – if you publish your own `security.txt`, reading it is fine. > If your app has a `Route::fallback()` that returns 200 (e.g. an SPA), matching requests never return 404, > so automatic blocking won't trigger for them. Default patterns include `.well-known/security.txt`, `security.txt`, `.env*`, `.git/*`, `wp-login.php`, `wp-admin*`, `xmlrpc.php`, `phpmyadmin*`, `vendor/phpunit/*` and `*.php`. Ordinary 404s (e.g. `/missing-page`) never trigger a block. Tune it in `config/bad-ip-blocker.php`: | Key | Default | Description | | --- | --- | --- | | `auto_block.enabled` | `true` | Env `BAD_IP_BLOCKER_AUTO_BLOCK` | | `auto_block.paths` | see above | `Request::is()` patterns | | `auto_block.max_hits` | `1` | Matching 404s before blocking | | `auto_block.decay` | `3600` | Seconds a hit counter is kept | | `auto_block.ttl` | `604800` (7 days) | Env `BAD_IP_BLOCKER_AUTO_BLOCK_TTL`, `null` = permanent | ## Configuration | Key | Default | | --- | --- | | `enabled` | `true` (env `BAD_IP_BLOCKER_ENABLED`) | | `auto_register` | `true` (env `BAD_IP_BLOCKER_AUTO_REGISTER`) | | `schedule_prune` | `true` (env `BAD_IP_BLOCKER_SCHEDULE_PRUNE`) | | `storage_path` | `storage/app/bad-ip-blocker.json` (env `BAD_IP_BLOCKER_STORAGE_PATH`) | | `response.status` / `response.message` | `418` / `Blocked.` | | `whitelist` | loopback + private ranges (IPs or CIDR) | The list is a plain JSON file per server. If you run several servers or containers, point `storage_path` at persistent, shared storage – otherwise each instance has its own list and a container's list is lost on redeploy. ## Example response when blocked ``` HTTP/1.1 418 I'm a teapot Content-Type: text/html; charset=UTF-8 Blocked. ``` ## Testing ```bash composer test ``` ## License MIT License © Mattitja AB