2026-11-11 23:19:20 +01:00
2026-11-11 23:19:20 +01:00
2026-11-11 23:19:20 +01:00
2026-11-11 23:19:20 +01:00
2026-11-11 23:19:20 +01:00
2026-11-11 23:19:20 +01:00
2026-11-11 23:19:20 +01:00
2026-11-11 23:19:20 +01:00
2026-11-11 23:19:20 +01:00

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

composer require mattitja/bad-ip-blocker

Publish the config (optional):

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:

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:

->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

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

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

composer test

License

MIT License © Mattitja AB

S
Description
No description provided
Readme MIT
44 KiB
0 Stars 1 Watchers 0 Forks
v1.0.0
Latest
2026-11-11 22:20:56 +00:00
Languages
PHP 100%