Bot blocking
The middleware rejects requests by User-Agent and by IP address. Every setting on this page lives in config/livewire-injection-stopper.php. Publish that file first:
php artisan vendor:publish --tag=livewire-injection-stopper-config
Which User-Agents are blocked
blocked_user_agents is a list of patterns. A request is blocked when its User-Agent header contains one of them. The comparison is case-insensitive and uses str_contains(). So python blocks python-requests/2.31, Python-urllib/3.11 and every other client that names Python. curl/ blocks curl/8.4.0 but not a User-Agent that has the word “curl” without the slash.
The defaults, 27 patterns in two groups:
| Group | Patterns |
|---|---|
| HTTP clients and scripts | python, aiohttp, httpx, curl/, wget, scrapy, postman, insomnia, httpie, go-http-client, java/, okhttp, axios, node-fetch, libwww-perl |
| SEO and AI crawlers | ahrefsbot, semrushbot, dotbot, mj12bot, blexbot, dataforseo, bytespider, petalbot, gptbot, claudebot, ccbot, anthropic |
The default list holds no search engine and no generic word such as bot, spider or crawler. Those words are part of Googlebot and bingbot, so adding one blocks them too.
Two limits:
- A request without a
User-Agentheader, or with an empty one, is never blocked by this check. - A client can send any User-Agent it likes. A bot that sends a browser string passes.
Let a client through with the allow list
allowed_user_agents wins over blocked_user_agents. A User-Agent that contains an allowed pattern passes this check, whatever else it contains. The defaults are sentryuptimebot, uptimerobot, pingdom and statuscake.
If you add a generic word to the blocked list, add your uptime monitor and the search engines to the allowed list first:
// config/livewire-injection-stopper.php
'allowed_user_agents' => [
'uptimerobot',
'googlebot',
'bingbot',
],
'blocked_user_agents' => [
// ... the defaults
'bot',
],
Because a client chooses its own User-Agent, a script that sends an allowed pattern passes this check as well.
Which IP addresses are blocked
blocked_ips holds exact addresses. The default list is empty. There is no CIDR or wildcard matching; for ranges, use your firewall or a proxy rule.
// config/livewire-injection-stopper.php
'blocked_ips' => [
'203.0.113.42',
],
The address comes from $request->ip(). When the application sits behind a load balancer or a proxy, configure Laravel’s trusted proxies. Without that, $request->ip() returns the address of the proxy for every request.
Paths that skip every check
whitelist_routes lists paths that skip every check: the IP check, the User-Agent check and the payload inspection. Use it for webhooks (URLs that another service calls with a scripted client).
Patterns are matched with PHP’s fnmatch() against the request path without a leading slash. * also matches slashes, so api/webhooks/* matches api/webhooks/stripe and api/webhooks/stripe/events, but not api/webhooks itself.
// config/livewire-injection-stopper.php
'whitelist_routes' => [
'api/mollie-webhook',
'api/webhooks/*',
],
The two defaults are examples; replace them with your own paths. Keep the patterns narrow: a pattern such as * turns the filter off.
The whitelist only matters for routes the middleware runs on. Routes outside the web middleware group, such as everything in routes/api.php, are not filtered in the first place.
The response a blocked request gets
// config/livewire-injection-stopper.php
'response_message' => 'Access Denied',
'response_status' => 403,
response_message is the response body. It is sent with Laravel’s default Content-Type, text/html; charset=utf-8, so keep it plain text. response_status can be any HTTP status; 404 makes a blocked request look like a missing page.
The log line
With log_blocked_requests enabled (the default), every blocked request writes a warning to the default log channel. In a default Laravel application that is storage/logs/laravel.log:
[2026-09-21 10:45:11] production.WARNING: [LivewireInjectionStopper] Blocked User-Agent: python-requests/2.31.0 {"reason":"blocked_user_agent","ip":"203.0.113.42","user_agent":"python-requests/2.31.0","url":"https://example.com/contact","method":"POST"}
The four messages are:
[LivewireInjectionStopper] Blocked IP: 203.0.113.42[LivewireInjectionStopper] Blocked User-Agent: python-requests/2.31.0[LivewireInjectionStopper] Suspicious Livewire payload detected[LivewireInjectionStopper] Blocked Livewire property manipulation attempt
The package treats these messages as public API and keeps them the same within 1.x, so a log alert keeps matching.
The RequestBlocked event
Darvis\LivewireInjectionStopper\Events\RequestBlocked is dispatched for every blocked request and every silenced exception, whether logging is on or off. It carries:
| Property | Value |
|---|---|
reason | blocked_ip, blocked_user_agent, suspicious_payload or locked_property. Also available as the constants RequestBlocked::BLOCKED_IP, BLOCKED_USER_AGENT, SUSPICIOUS_PAYLOAD and LOCKED_PROPERTY |
ip | the client IP, or null |
userAgent | the User-Agent header, or null |
url | the full URL of the request |
exception | the silenced exception, or null for a block by the request filter |
<?php
// app/Providers/AppServiceProvider.php, inside boot()
use Darvis\LivewireInjectionStopper\Events\RequestBlocked;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Event;
Event::listen(RequestBlocked::class, function (RequestBlocked $event): void {
if ($event->reason === RequestBlocked::BLOCKED_IP) {
return;
}
Cache::increment('blocked-requests:'.now()->format('Y-m-d'));
});
This listener counts every block per day, except the ones for a listed IP address.
Applying the middleware elsewhere
The middleware is appended to the web group automatically. For another group or a single route, use the alias:
<?php
// routes/api.php
use Illuminate\Support\Facades\Route;
Route::middleware('livewire-injection-stopper')->group(function () {
// ...
});
Every route in this group now gets the same four checks.
There is no master switch. To turn the request filter off without removing the package, empty blocked_user_agents and blocked_ips and set check_payload_injection to false.
Checking a request yourself
The LivewireInjectionStopper facade (a static shortcut to the manager class) exposes the checks. check($request) runs all four and returns the reason or null. The individual checks are there too:
<?php
use Darvis\LivewireInjectionStopper\Facades\LivewireInjectionStopper;
LivewireInjectionStopper::check($request); // 'blocked_ip', ... or null
LivewireInjectionStopper::isBlockedUserAgent($request->userAgent()); // bool
LivewireInjectionStopper::isBlockedIp($request->ip()); // bool
LivewireInjectionStopper::isWhitelisted($request->path()); // bool
LivewireInjectionStopper::hasSuspiciousPayload($request); // bool
isBlockedUserAgent(), isBlockedIp() and hasSuspiciousPayload() do not look at the whitelist; only check() does.