keepsuit / laravel-threat-blocker
Block threat request to your application
Package info
github.com/keepsuit/laravel-threat-blocker
pkg:composer/keepsuit/laravel-threat-blocker
Requires
- php: ^8.3
- illuminate/contracts: ^12.0 || ^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/ai: ^1.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.8 || ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- spatie/invade: ^2.1
- spatie/laravel-honeypot: ^4.5
- spatie/laravel-ray: ^1.35
- spatie/test-time: ^1.3
Suggests
- laravel/ai: Required for AI spam detector
- spatie/laravel-honeypot: Required for form honeypot detector
Provides
None
Conflicts
None
Replaces
None
- dev-main
- 0.2.1
- 0.2.0
- 0.1.3
- 0.1.2
- 0.1.1
- 0.1.0
- dev-fix/ai-spam-random-text
- dev-docs/upgrade-guide-version
- dev-feature/uniform-detectors
- dev-feature/ai-spam-detector
- dev-chore/drop-laravel-11
- dev-feature/email-reputation-detector
- dev-feat/repeated-payload-detector
- dev-feature/feat/strict-honeypot-requirements
This package is auto-updated.
Last update: 2026-10-02 12:29:52 UTC
README
Laravel Threat Blocker is a package to block threat requests to your Laravel application based on different rules.
Installation
You can install the package via composer:
composer require keepsuit/laravel-threat-blocker
You can publish the config file with:
php artisan vendor:publish --tag="laravel-threat-blocker-config"
Usage
-
Add the
ProtectAgainstThreatsmiddleware to routes you want to protect:use Keepsuit\ThreatBlocker\Middleware\ProtectAgainstThreats; Route::post('contact', [ContactController::class, 'submit'])->middleware(ProtectAgainstThreats::class);
-
Run the update command to warm the detectors cache:
php artisan threat-blocker:update
-
Schedule the update command to run periodically (e.g., daily) using Laravel's task scheduling:
$schedule->command('threat-blocker:update')->daily();
Configuration
This is the contents of the published config file:
return [ /** * This option enables or disables the Threat Blocker protection. */ 'enabled' => env('THREAT_BLOCKER_ENABLED', true), /** * HTTP methods checked by the detectors (names or HttpMethod cases), '*' means any method. * Each detector can override it with its own 'methods' option. * Detectors that inspect the request body (FormHoneypotDetector, AiSpamDetector, * EmailReputationDetector) only support POST, PUT and PATCH, other methods are ignored. */ 'methods' => ['POST'], /** * Storage driver to use for caching detectors data. */ 'storage_driver' => env('THREAT_BLOCKER_STORAGE_DRIVER', 'cache'), 'storage' => [ 'cache' => [ 'store' => env('THREAT_BLOCKER_CACHE_STORE', env('CACHE_STORE', env('CACHE_DRIVER', 'file'))), 'prefix' => env('THREAT_BLOCKER_CACHE_PREFIX', 'threat_blocker'), ], ], /* * The responder class that will be used to respond to detected threats. * You can create your own responder by implementing the Keepsuit\ThreatBlocker\Contracts\ThreatResponder interface. */ 'responder' => \Keepsuit\ThreatBlocker\Responders\BlankPageResponder::class, /** * The following list of "detectors" will be used to identify threats. * You can enable or disable each detector individually and configure their settings. */ 'detectors' => [ /** * Block requests coming from IPs listed in the AbuseIPDB database. */ \Keepsuit\ThreatBlocker\Detectors\AbuseIpDetector::class => [ 'enabled' => env('THREAT_BLOCKER_ABUSE_IP_DETECTOR_ENABLED', true), // Source url for AbuseIP data, it can be a custom url or one of the predefined sources (provided by https://github.com/borestad/blocklist-abuseipdb) 'source' => \Keepsuit\ThreatBlocker\Enums\AbuseIpSource::Days60->url(), 'blacklist' => [ // These IPs will always be blocked by the AbuseIpDetector ], 'whitelist' => [ // These IPs will never be blocked by the AbuseIpDetector '127.0.0.1', ], ], /** * Block registrations using disposable or undeliverable email domains. */ \Keepsuit\ThreatBlocker\Detectors\EmailReputationDetector::class => [ 'enabled' => env('THREAT_BLOCKER_EMAIL_REPUTATION_DETECTOR_ENABLED', true), // Source URL for the disposable email domain list, one domain per line. 'source' => \Keepsuit\ThreatBlocker\Enums\EmailReputationSource::DisposableEmailDomains->url(), // Empty fields disable this detector. `email` also matches nested terminal fields; // use patterns such as `contacts.*.email` for a specific nested path. 'fields' => ['email'], 'check_mx' => true, 'mx_cache_ttl' => 86400, 'blacklist' => [], 'whitelist' => [], ], /** * Block POST requests that look like automated form submissions based on their headers. */ \Keepsuit\ThreatBlocker\Detectors\BotSignatureDetector::class => [ 'enabled' => env('THREAT_BLOCKER_BOT_SIGNATURE_DETECTOR_ENABLED', true), 'rules' => [ 'missing_user_agent' => true, 'known_bot_user_agents' => true, 'missing_accept_language' => false, 'invalid_referer' => false, ], // Replace the defaults, or use array_merge() to extend them. 'user_agent_patterns' => \Keepsuit\ThreatBlocker\Detectors\BotSignatureDetector::DEFAULT_USER_AGENT_PATTERNS, ], /** * Block requests that contain form submissions with honeypot fields filled out. * This detector requires spatie/laravel-honeypot package to be installed and configured. */ \Keepsuit\ThreatBlocker\Detectors\FormHoneypotDetector::class => [ 'enabled' => env('THREAT_BLOCKER_FORM_HONEYPOT_DETECTOR_ENABLED', true), // Require honeypot fields on every POST request or selected URI patterns. // Examples: true, ['/contact', '/newsletter/*'] 'strict' => false, ], /** * Block POST requests classified as spam or phishing by an AI model. */ \Keepsuit\ThreatBlocker\Detectors\AiSpamDetector::class => [ 'enabled' => env('THREAT_BLOCKER_AI_SPAM_DETECTOR_ENABLED', false), 'provider' => env('THREAT_BLOCKER_AI_SPAM_DETECTOR_PROVIDER'), 'model' => env('THREAT_BLOCKER_AI_SPAM_DETECTOR_MODEL'), 'fields' => ['*'], 'only' => [], 'context' => [], 'threshold' => env('THREAT_BLOCKER_AI_SPAM_DETECTOR_THRESHOLD', 0.8), 'max_length' => 4000, 'timeout' => 5, ], ], ];
Storage
Detectors keep their data (the downloaded lists and the MX lookup results) in a storage driver. The only driver
is cache, which uses a Laravel cache store: set THREAT_BLOCKER_CACHE_STORE (it falls back to CACHE_STORE)
and, if needed, THREAT_BLOCKER_CACHE_PREFIX. The lists expire after one year without updates (every update renews them).
Use a persistent store shared by all your servers (e.g. redis or database) and not array.
Responder
The responder decides what a request blocked by a detector receives. The package provides:
BlankPageResponder(default) answers with an empty200response.ForbiddenResponderaborts with a403response.
To customize it, implement Keepsuit\ThreatBlocker\Contracts\ThreatResponder and set it as the responder.
$next lets the request proceed, which is useful to only monitor threats through the event:
use Illuminate\Http\Request; use Keepsuit\ThreatBlocker\Contracts\ThreatResponder; class RedirectResponder implements ThreatResponder { public function respond(Request $request, \Closure $next): mixed { return redirect()->back()->withErrors('Your request could not be processed.'); } }
HTTP methods
Detectors run only on the methods listed in the global methods option (default ['POST']),
and * means any method. Methods are case-insensitive names or Keepsuit\ThreatBlocker\Enums\HttpMethod cases. A detector can override it with its own methods option:
'methods' => ['POST'], 'detectors' => [ AbuseIpDetector::class => [ 'methods' => ['*'], ], ],
FormHoneypotDetector, AiSpamDetector and EmailReputationDetector inspect the request body, so they
only support POST, PUT and PATCH: any other configured method is ignored. An empty list means the
detector never runs; use 'enabled' => false to turn it off.
Detectors
Detectors run in the order of the detectors option, and the first one that detects a threat blocks the request.
Detectors that download a list (AbuseIpDetector, EmailReputationDetector) refresh it with
php artisan threat-blocker:update, and in the background when it is older than 3 days.
AbuseIpDetector
Blocks requests coming from the IPs of the AbuseIPDB blocklist maintained by borestad/blocklist-abuseipdb.
sourceis one of theAbuseIpSourceurls (Days60,Days30,Days14,Days7) or a custom url with one IP per line.blacklistIPs are always blocked,whitelistIPs are never blocked (it wins over the blacklist and the list).- The list contains IPv4 addresses only: an IPv6 address is blocked just when it is in
blacklist.
It does not read the request body, so it can run on any method: set 'methods' => ['*'] to check them all.
EmailReputationDetector
Blocks registrations using disposable or undeliverable email domains. It reads the request body.
EmailReputationSource provides built-in disposable-domain list URLs for the default,
DNS-validated, curated, and high-coverage sources. The source option also accepts any
custom URL serving one domain per line.
fieldsare the input fields to check. An empty list disables the detector.emailalso matches nested fields with that name, use patterns such ascontacts.*.emailfor a specific nested path.blacklistdomains are always blocked,whitelistdomains are never blocked.check_mxblocks domains without an MX record, the DNS result is cached formx_cache_ttlseconds.
BotSignatureDetector
BotSignatureDetector reads only the request headers, so it can run on any method. Each configured rule is independent:
missing_user_agentblocks missing or blank User-Agent headers.known_bot_user_agentsmatches the configurable case-insensitive PCRE patterns.missing_accept_languageblocks missing or blank Accept-Language headers.invalid_refererblocks missing, malformed, or cross-host Referer headers.
The default User-Agent patterns are conservative and available through
BotSignatureDetector::DEFAULT_USER_AGENT_PATTERNS. Supplying user_agent_patterns
replaces them; extend them with array_merge() when needed. Crawler and link-preview
identities such as Googlebot, bingbot, Slackbot, and Discordbot are intentionally not
included in the defaults.
FormHoneypotDetector
Blocks form submissions with a filled honeypot field (or, if valid_from_timestamp is enabled in the Spatie config, submitted too fast). It reads the request body and
needs spatie/laravel-honeypot installed and its component added to
your forms; without it the check is skipped and a warning is logged.
- The check runs even if
honeypot.enabledisfalsein the Spatie config. strict=>false(default) checks only requests that contain the honeypot fields,truealso blocks requests without them, and a list of URI patterns (e.g.['/contact', '/newsletter/*']) requires them only there.
AiSpamDetector
AiSpamDetector classifies form data as legitimate, spam or phishing with
laravel/ai. It is disabled by default and needs
composer require laravel/ai plus the API key of a provider that supports classification (only typesafe and openrouter do). Every evaluated request adds latency and provider cost,
so restrict it with only and keep it last in the detectors list.
- Form data is sent to the external provider. Files,
_token,_method, password fields and the honeypot timestamp field configured viahoneypot.valid_from_field_nameare never sent, and the payload is truncated tomax_lengthcharacters. - A request is blocked when the spam + phishing probability reaches
threshold. Providers that return no probabilities never block. - On any provider error or timeout the request is allowed and a warning is logged (fail-open).
contextmaps URI patterns to extra instructions, appended to the default ones:
'only' => ['/contact', '/quote/*'], 'context' => [ '/quote/*' => 'This form receives quote requests for industrial machinery.', ],
Models tested on OpenRouter:
| Model | Recommended | Test results / limitations |
|---|---|---|
~typesafe/jev-latest |
✅ | Passes the live tests. |
inception/mercury-decide:free |
✅ | Passes the live tests. |
liquid/d1 |
✅ | Passes the live tests. |
togethercomputer/tev1-4b-experimental |
❌ | Fails some live tests or scores close to the threshold. |
jaredpalmer/kev-4b |
❌ | Fails some live tests or scores close to the threshold. |
respan/span-01 |
❌ | Only supports Noul (yes/no) questions; returns an error for the detector's Choice question. |
respan/span-01-lite |
❌ | Only supports Noul (yes/no) questions; returns an error for the detector's Choice question. |
The live tests cover legitimate, spam and phishing submissions in Italian and English; validate the selected model with your own form data before production use.
Events and logging
Detectors report a threat by throwing ThreatDetectedException, which exposes the detectorId
(e.g. bot-signature) and a context array with details about the detection. The message is prefixed with
the detector id ([bot-signature] Known bot User-Agent detected.).
The ThreatDetectedEvent event carries the request and the exception. Detectors put in the
context only derived values (category, score, domain), never request content or full email addresses.
The package does not log detections, listen to the event to log them the way your application needs (level, channel, request data):
use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Log; use Keepsuit\ThreatBlocker\Events\ThreatDetectedEvent; Event::listen(function (ThreatDetectedEvent $event) { Log::warning($event->exception->getMessage(), [ 'method' => $event->request->method(), 'path' => $event->request->path(), 'ip' => $event->request->ip(), ...$event->exception->context, ]); });
Testing
composer test
Live tests that call the real provider are excluded from the default run. Run them with
vendor/bin/pest --group=live with TYPESAFE_API_KEY (or OPENROUTER_API_KEY) set in the shell or in the package .env; without a key they are skipped.
THREAT_BLOCKER_AI_SPAM_DETECTOR_PROVIDER and THREAT_BLOCKER_AI_SPAM_DETECTOR_MODEL select another provider or model.
Changelog
Please see CHANGELOG for more information on what has changed recently.
Credits
License
The MIT License (MIT). Please see License File for more information.