edulazaro / wirefence
Fences Livewire apps against tampered requests: a property sent a value no form can produce, a locked property, an unknown property or method. Answers 400 instead of a 500 in the log, and blocks the address for a while.
Requires
- php: >=8.2
- illuminate/database: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- livewire/livewire: ^3.6.4|^4.0
Requires (Dev)
- orchestra/testbench: ^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
wirefence
A fence for Livewire applications. Bots probe every Livewire site they find by posting forged updates to its update endpoint: an array where a form sends text, a locked property, a property or method the component does not have. Each one used to reach your log as a 500, burying your real errors under pages of Cannot assign array to property ... of type string. wirefence answers them with a 400, writes one warning line and blocks the address from the whole application for a day.
Laravel 12 and 13 · Livewire 3.6.4 and up, and 4 · Volt included · no JavaScript · one table.
Install
The package is auto-discovered, so installing it and migrating its table is all it takes.
composer require edulazaro/wirefence php artisan migrate
It requires Livewire 3.6.4 or later on purpose: earlier versions carry CVE-2025-54068, the remote code execution these bots were looking for in the first place.
What gives a request away
Each check looks at the request before Livewire assigns or calls anything, through Livewire's own hooks, so it behaves the same on Livewire 3 and 4. None of them can be tripped from a page:
| Signal | What arrived | Blocks after |
|---|---|---|
type_mismatch |
an array or object for a property typed string, int, float or bool (nullable or in a union of those) |
1 |
locked_property |
an update to a #[Locked] property |
1 |
unknown_property |
an update to a property the component does not have, or that is not public | 3 |
unknown_method |
a call to a method the component does not have, or that is not public | 3 |
The last two take three strikes within ten minutes because a tab left open across a deploy that renamed a property or method can send the old name once or twice. Livewire's internal calls ($refresh, $set, __dispatch, _startUpload...) are never counted.
Text typed into a number field is a person's mistake, not a probe, and is left to Livewire. So is any TypeError your own component code raises.
What happens then
Every tampered request is answered with a 400 and one warning line in the log, with the address, the signal, the component and the property or method.
Once an address reaches a signal's threshold it is blocked for 24 hours: every request it makes to the application, Livewire or not, gets a 403 from the first middleware, before sessions, the database or Livewire. A ClientBlocked event is dispatched, for a mail or a Slack message:
Event::listen(\EduLazaro\Wirefence\Events\ClientBlocked::class, function ($event) { // $event->ip, ->signal, ->component, ->target, ->until, ->strikes });
Addresses it never blocks
Blocking the wrong address would lock out real users, so some are never blocked, only answered with the 400.
- Private and reserved addresses. Seeing one means the application does not see its visitors' real address, and blocking it would block everybody behind it. A warning in the log says so.
- The
allowlist, addresses or CIDR ranges. - Cloudflare's own ranges, when the Cloudflare option is on.
wirefence trusts the address Laravel gives it, so configure trusted proxies if a proxy or load balancer sits in front. Behind Cloudflare without that, turn on WIREFENCE_CLOUDFLARE=true: a request coming from one of Cloudflare's ranges is then judged by its CF-Connecting-IP header, which nobody else can use to impersonate a visitor.
Commands
Three commands cover what an administrator needs.
php artisan wirefence:list # blocked right now (--all for the history) php artisan wirefence:unblock 203.0.113.7 php artisan wirefence:prune # delete old records now
Storage
The cache answers "is this address blocked" on every request, so a visitor who is not costs one cache lookup and nothing else. The wirefence_blocks table is the record behind it: one row per address, with what gave it away the last time, how many times it has been blocked and until when. If the cache is cleared, a blocked bot gets one more try, and that try blocks it again.
An address is personal data, so rows whose block ended more than 30 days ago are deleted every day. That runs through Laravel's scheduler, so it needs schedule:run in your crontab; without it, run wirefence:prune by hand. Blocking works either way.
Configuration
Every threshold and duration can be changed by publishing the config file.
php artisan vendor:publish --tag=wirefence-config
| Key | Default | |
|---|---|---|
enabled |
true (WIREFENCE_ENABLED) |
off, Livewire behaves as without the package |
signals.* |
1, 1, 3, 3 | strikes before blocking; 0 turns a signal off |
window_minutes |
10 | the window strikes are counted in |
block_minutes |
1440 | how long a block lasts |
status / blocked_status |
400 / 403 | the answers |
keep_days |
30 | days a record stays after its block ended; null keeps them |
allow |
[] |
addresses or ranges never blocked |
cloudflare.enabled |
false (WIREFENCE_CLOUDFLARE) |
judge Cloudflare's requests by CF-Connecting-IP |
cache_store / log_channel |
null |
the application's defaults |
A caveat for your components
A property typed string bound to something that sends an array, such as a <select multiple> or a group of checkboxes, is a bug in the component, and wirefence will take its requests for probes. Type such properties array.
Tests
The tests post to Livewire's real update endpoint with a real snapshot, forging only what a bot forges.
composer install vendor/bin/phpunit
Sponsors
wirefence is supported by the following sponsors. Thank you for keeping it growing:
Author
Created by Edu Lazaro
License
wirefence is open-sourced software licensed under the MIT license.
