darvis/livewire-injection-stopper
Protect your Laravel + Livewire app from spam bots and injection attacks. This package blocks automated bots, audits Livewire components for manipulable public properties, and silences bot-triggered exceptions so your Sentry logs stay clean.
Installation:
composer require darvis/livewire-injection-stopper
The middleware registers itself into the web group — you do not assign it to individual routes.
Read three pages, in this order (new in v1.3.1):
First use case — confirm a block, then audit:
php artisan livewire:audit
Then follow the Testing page to reproduce a blocked update request. Expect output ending in a Total: line plus a 📖 See https://livewire.laravel.com/docs/locked … link.
Where to look first:
block_all_array_injections, scalar_properties, whitelist_routes).#[Locked] gaps.Blocking top-level array injection (the main event):
The middleware inspects Livewire update requests and rejects top-level array keys used to smuggle state (e.g. activeFilters[tags]=...). It sits at the end of the web group, so:
User-Agent are never blocked by the default rules.Route whitelisting:
// config/livewire-injection-stopper.php
'whitelist_routes' => [
'admin/*', // '*' also matches slashes — 'admin/*' covers admin/users/1/edit
'webhooks/stripe',
],
Note that * matches across /, so a pattern like admin/* is broader than it looks.
Tightening scalar_properties:
'scalar_properties' => [
'email', // must be lowercase
'firstname', // NOT 'firstName' — see Gotchas
'form.email', // whole-name match; 'email' does not cover 'form.email'
],
Entries are compared against the whole property name after lowercasing the name. The entries themselves are not lowercased for you.
Audit in CI/CD:
# .github/workflows/security.yml
- run: composer install
- run: php artisan livewire:audit
Pair it with a scheduled run so drift from newly added public properties gets caught:
$schedule->command('livewire:audit')->weekly();
Treat a non-empty Total: as a failure signal; do not treat ✅ No security issues found! as proof (see Gotchas).
Error handling — let the package do it:
Since 1.3.0 the package hooks Livewire's dontReport() and reportable() only. You don't register a custom exception handler class; the block response comes from the middleware. If you want visibility, add a dedicated log channel and check it during incident triage:
'channels' => [
'injection_attempts' => [
'driver' => 'single',
'path' => storage_path('logs/injection_attempts.log'),
'level' => 'warning',
],
],
#[Locked] on any property a client must not set. The audit exists to find the ones you forgot; the attribute is the fix.app/Livewire if you want them audited (see the scan limits in Gotchas).scalar_properties entries must be lowercase.
The property name is strtolower'd before comparison, but your config entries are not. firstName will never match $firstName. Write firstname.
scalar_properties matches the whole name.
'email' does not cover form.email. List every fully-qualified name you need.
Nested/dotted names are only partly exempt.
A name containing a dot skips the block_all_array_injections rule — but it is still blocked when the whole name starts with one of the fixed prefixes, e.g. activeFilters.tags, hidden.0. Don't assume dotted means safe.
A published config list replaces the defaults entirely.
Publishing and editing whitelist_routes (or any list) drops the package's defaults rather than merging. Copy the defaults across before you edit.
CSRF fires first.
Because the middleware is at the end of the web group, a POST without a valid token gets Laravel's 419 before the package ever runs. A 419 is not evidence that blocking works.
No User-Agent = never blocked.
Requests without a UA bypass the bot rules. If you're testing with curl, remember -A.
The audit silently skips a lot. It does not report:
extends Component (e.g. your own intermediate base class),float, array, ?string, ?int, and any namespaced class type,#[Locked, Url],#[Locked] public bool $x = false; — that line is not recognised at all, so the property is simply invisible to the scan (not a false positive, a false negative).This is now documented on the Security audit page as an explicit list of limits — read it before trusting a clean run.
✅ No security issues found! is not proof.
The same message is printed when app/Livewire doesn't exist. Always check the directory exists and the Total: line makes sense for your component count.
Livewire::test() does not exercise the middleware.
Component tests do go through the HTTP layer, but with all middleware disabled (including CSRF token verification — don't send one). That means a green component test does not demonstrate that a top-level array update is blocked in a real browser. Use the feature test on the Testing page for that claim.
Debug mode changes Livewire 4's behaviour.
"Livewire rejects wrong-type values with a 419" is true only outside debug mode. In debug mode it rethrows the TypeError. Don't write an assertion that holds in one mode only.
The block response is text/html, Laravel's default Content-Type: text/html; charset=utf-8 — not plain text. Assert accordingly.
storage/logs/laravel.log against them.$middleware (it's appended to web, not to the global stack, so it's easy to miss).extends Component? After adding components, composer dump-autoload.User-Agent is present, the route isn't matched by a * whitelist pattern, and the name doesn't start with one of the fixed prefixes.whitelist_routes, scalar_properties and block_all_array_injections cover most day-to-day needs without subclassing anything.#[Locked] is the primary hardening tool; the audit and the block are complementary. The audit finds missing locks; the middleware stops the injection attempts that get past them.How can I help you explore Laravel packages today?