Files
bad-ip-blocker/README.md
T
2026-11-11 23:19:20 +01:00

143 lines
4.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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