Search by

mattitja / bad-ip-blocker

Mattitja

Block bad IPs in Laravel using a local JSON list with automatic blocking of scanners.

Package info

git.mattitja.se/559342-9607/bad-ip-blocker.git

pkg:composer/mattitja/bad-ip-blocker

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

v1.0.0 2026-11-11 22:19 UTC

This package is not auto-updated.

Last update: 2026-11-12 20:45:16 UTC


README

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:

KeyDefaultDescription
auto_block.enabledtrueEnv BAD_IP_BLOCKER_AUTO_BLOCK
auto_block.pathssee aboveRequest::is() patterns
auto_block.max_hits1Matching 404s before blocking
auto_block.decay3600Seconds a hit counter is kept
auto_block.ttl604800 (7 days)Env BAD_IP_BLOCKER_AUTO_BLOCK_TTL, null = permanent

Configuration

KeyDefault
enabledtrue (env BAD_IP_BLOCKER_ENABLED)
auto_registertrue (env BAD_IP_BLOCKER_AUTO_REGISTER)
schedule_prunetrue (env BAD_IP_BLOCKER_SCHEDULE_PRUNE)
storage_pathstorage/app/bad-ip-blocker.json (env BAD_IP_BLOCKER_STORAGE_PATH)
response.status / response.message418 / Blocked.
whitelistloopback + 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