Livewire Security: A Practical Guide for Livewire 4 (2026)
Livewire makes it easy to build dynamic, reactive interfaces without writing much JavaScript. That's exactly why it's so easy to forget what's happening under the hood. Every Livewire component runs over HTTP, and a lot of what feels like private server-side state is actually sent to, and editable by, the browser. That gap between how Livewire feels and how it actually works is where most Livewire security problems come from.
This guide covers how to secure a Livewire 4 application: the mental model to keep in your head, the specific protections Livewire gives you, and the practical habits that keep components safe. It also looks back at the two real vulnerabilities that hit Livewire 3, including the critical remote code execution flaw that ended up in CISA's exploited-vulnerabilities catalog, because they're a useful reminder of what's at stake.
The Short Version
If you only read one section, read this one:
- Treat every public property as untrusted user input. Validate it the way you would form input.
- Lock properties the client should never change with the
#[Locked]attribute. - Authorise every action on the server, inside the method, not just in the UI that calls it.
- Never trust action parameters (IDs and the like). Check ownership and permissions every time.
- Validate file uploads properly and never store them under the user's original filename on a public disk.
- Keep Livewire patched and run
composer audit. Livewire 3 shipped two serious remote code execution bugs, both fixed in point releases. - Livewire 4's security model is the same as v3's. Upgrading is good for features and performance, but it isn't a substitute for the practices above.
Livewire Runs Over HTTP, and the Client Is Not Your Friend
The single most important thing to understand about Livewire security is that every interaction is a normal HTTP request to your backend. When a user types in a field, clicks a button, or triggers any other Livewire action, the browser sends a request to the /livewire/update endpoint with the component's current state. Your server rebuilds the component, applies the changes, and sends back the new state.
One thing you don't need to worry about: those requests go through Laravel's web middleware group, so they're CSRF-protected and session-aware just like any other route. Livewire handles the token for you. The risk isn't forged requests from another origin, it's the legitimate origin (the user's own browser) sending things you didn't expect.
Because of that round trip, two things are true that catch people out.
Public Properties Are Hidden Form Fields
Public properties are serialised to JSON and sent to the browser, which means the user can read and change them. They look like ordinary PHP class properties in your code, but functionally they behave like hidden form inputs. Stephen Rees-Carter put it well in his Securing Laravel writeup on this exact topic: "Public Properties may look like PHP class properties, but they're really hidden form fields, just waiting for your input."
You don't even need the UI to manipulate them. If a component has a public $title property, an attacker can sit in the browser console and run:
1Livewire.first().set('title', 'PWNED')
It doesn't matter whether there's an input bound to that property on the page. If it's public, it's editable.
Every Public Method Is Callable
The same applies to your component's methods. Any public method can be invoked from the client, whether or not you've wired it up to a wire:click or any other handler. The Livewire documentation is blunt about this: "Every public method inside your Livewire component is callable from the client. Even methods you haven't referenced inside a wire:click handler."
So hiding a delete button behind an @if (auth()->user()->isAdmin()) check does nothing to stop a non-admin calling the method directly. The UI is a convenience, not a security boundary.
Hold onto those two facts. Almost every practice below follows directly from them.
Securing Your Livewire Components
1. Validate Public Properties
Since public properties are user input, validate them before you do anything with them. Livewire's #[Validate] attribute lets you colocate the rules with the property, and $this->validate() runs them before you persist:
1use App\Models\Post; 2use Livewire\Attributes\Validate; 3use Livewire\Component; 4 5class CreatePost extends Component 6{ 7 #[Validate('required|min:3|max:255')] 8 public string $title = ''; 9 10 #[Validate('required|min:10')]11 public string $content = '';12 13 public function save()14 {15 $this->validate();16 17 Post::create([18 'title' => $this->title,19 'content' => $this->content,20 ]);21 }22}
If you need rules that can't be expressed in a PHP attribute (a Rule object, conditional logic), define a rules() method instead and call $this->validate() as usual. The important part is that validation runs server-side on every save, not just in the browser.
2. Lock Properties That Must Not Change
Some properties exist purely to carry server-side context and should never be touched by the client. The classic example is a record ID. Mark those with #[Locked]:
1use App\Models\Post; 2use Livewire\Attributes\Locked; 3use Livewire\Component; 4 5class EditPost extends Component 6{ 7 #[Locked] 8 public int $postId; 9 10 public string $title = '';11 12 public function mount(Post $post)13 {14 $this->postId = $post->id;15 $this->title = $post->title;16 }17}
If a user tries to tamper with a locked property, Livewire throws an error and the request fails. There's an important caveat from the docs, though: locking stops the client from changing the value, but it doesn't stop your own code assigning untrusted input to it. So don't lock a property and then set it from request data and call it safe.
A related point: if you store a full Eloquent model as a public property (rather than just its ID), Livewire treats it specially and the user can't swap it for a different model. Storing the model can be a cleaner option than juggling a locked ID, depending on the component.
3. Authorise Every Action on the Server
This is the big one. The Livewire docs call it out directly: "Arguably the most common security pitfall in Livewire is failing to validate and authorize Livewire action calls before persisting changes to the database."
Authorise inside the method, using a policy, every time:
1public function delete($id)2{3 $post = Post::findOrFail($id);4 5 $this->authorize('delete', $post);6 7 $post->delete();8}
The $this->authorize() call works because Livewire's base component uses Laravel's AuthorizesRequests trait. If the current user isn't allowed to delete that post, the policy throws and nothing happens. Crucially, this check runs no matter how the method was triggered, so a hand-crafted request from the console hits the same wall as a button click.
4. Don't Trust Action Parameters
Notice that in the example above we look the post up by $id and then authorise it. That ordering matters. Action parameters are untrusted, exactly like the rest of the request. A user can call delete(999) for any ID they like, regardless of what the markup says. Passing wire:click="delete({{ $post->id }})" in your Blade does not pin the parameter to that value; it's just a default the client is free to change.
The fix is always the same: resolve the model from the parameter, then run an authorization check that ties it back to the current user. Never assume the ID belongs to someone who's allowed to act on it.
5. Keep Internal Methods Protected or Private
Because every public method is callable, the simplest way to stop a helper being invoked from the outside is to not make it public. If a method is internal plumbing (a calculation, a state transition, anything you don't want triggered on demand), mark it protected or private. Only the methods that genuinely need to be called from the front end should be public.
6. Re-check Authorization with Persistent Middleware
Route middleware like can:update,post runs when the page first loads, but Livewire's subsequent update requests don't go back through your route definitions. Livewire handles this with persistent middleware: it automatically re-applies a set of auth-related middleware (including Authenticate and Authorize) on every component request, so a user whose permissions change mid-session gets re-checked.
The defaults cover the common auth middleware. If you rely on a custom middleware for authorization, register it so it persists too:
1// In a service provider's boot() method2use Livewire\Livewire;3 4Livewire::addPersistentMiddleware([5 \App\Http\Middleware\EnsureUserHasActiveSubscription::class,6]);
Without this, a custom guard that protected the initial page load silently stops applying to follow-up Livewire requests, which is a subtle way to leave a hole.
7. Validate File Uploads Carefully
File uploads deserve their own attention, because a careless upload handler is a direct path to running code on your server (more on a real-world example of this below). Validate uploads like any other property:
1use Livewire\Attributes\Validate; 2use Livewire\Component; 3use Livewire\WithFileUploads; 4 5class UploadAvatar extends Component 6{ 7 use WithFileUploads; 8 9 #[Validate('image|max:1024')] // 1MB max10 public $photo;11 12 public function save()13 {14 $this->validate();15 16 $this->photo->store(path: 'avatars');17 }18}
A few things worth doing:
- Let
store()generate the filename. It creates a random, hashed name. Storing a file under the user's original filename (getClientOriginalName()) on a public, PHP-executing disk is how upload bugs turn into code execution. - Validate the actual file, including type and size. You can also tighten the global rules in
config/livewire.php:
1'temporary_file_upload' => [2 'rules' => 'file|mimes:png,jpg,pdf|max:102400',3],
- Lean on the built-ins. Livewire serves uploads from temporary signed URLs that can't reach outside the temp directory, and it rate-limits uploads (
throttle:5,1, five per minute per user) by default. Don't undo those without a reason.
8. Keep Sensitive Data Out of the Payload
Public properties aren't just writable from the client, they're readable too. Every one of them is serialised to JSON and sent to the browser, so don't put anything in a public property that the current user shouldn't see: API keys, tokens, other people's records, internal flags. For state that should stay on the server, use a protected property, and for values you derive from sensitive data, use a #[Computed] property so the result is calculated on demand instead of being stored in (and shipped with) the component's state.
Livewire also includes your model class names in the serialised payload, which quietly reveals more about your application's internals than you might want. Laravel's morph map lets you alias them:
1use Illuminate\Database\Eloquent\Relations\Relation;2 3// In a service provider's boot() method4Relation::enforceMorphMap([5 'post' => \App\Models\Post::class,6 'user' => \App\Models\User::class,7]);
It's a small hardening step, but an easy one.
9. Rate-Limit Sensitive Actions
Livewire rate-limits file uploads out of the box, but it doesn't throttle your own actions. Since every public method is callable on demand, anything sensitive (a login attempt, an OTP send, a password check, a "contact us" submission) can be hammered as fast as the network allows. Apply Laravel's rate limiter inside those methods:
1use Illuminate\Support\Facades\RateLimiter; 2use Illuminate\Validation\ValidationException; 3 4public function login() 5{ 6 $key = 'login:'.request()->ip(); 7 8 if (RateLimiter::tooManyAttempts($key, maxAttempts: 5)) { 9 $seconds = RateLimiter::availableIn($key);10 11 throw ValidationException::withMessages([12 'email' => "Too many attempts. Please try again in {$seconds} seconds.",13 ]);14 }15 16 RateLimiter::hit($key, decaySeconds: 60);17 18 // ... attempt authentication, and call RateLimiter::clear($key) on success19}
This is the same RateLimiter you'd use anywhere else in Laravel, so the keys, decay, and lockout behaviour all work as you'd expect. The point is just to remember that a Livewire method is a public endpoint, and public endpoints that do expensive or security-sensitive work need a limit.
What Livewire 4 Changes for Security (and What It Doesn't)
Livewire 4 landed in early 2026, and it's a substantial release: single-file components are now the default, the new Blaze rendering engine is faster, the islands architecture (@island) lets you isolate expensive parts of a page, and there are quality-of-life additions like slots and a Livewire::visit() testing API. The upgrade from v3 is billed as having minimal breaking changes.
Here's the honest part, though: none of that is a security overhaul. The threat model and the protections are the same in v4 as they were in v3. Public properties are still client-editable, public methods are still callable, and the same attributes (#[Locked], #[Validate]) and the same $this->authorize() pattern are how you defend a component. Snapshot checksums, which raise a CorruptComponentPayloadException when a payload is tampered with, exist in both versions, and they remain a backstop rather than a replacement for your own validation and authorization.
So treat "we're on Livewire 4" as good housekeeping, not a security feature. A Livewire 4 component with an unauthorised delete() method is exactly as exploitable as the same component on v3. Everything in the previous section applies regardless of which major version you're running.
The Lessons from Livewire 3
Livewire 3 is worth a look back, because it shipped two genuinely serious vulnerabilities. Both were patched quickly, and the response to both was the same boring advice that always works: stay current.
CVE-2025-54068: Remote Code Execution via Property Hydration
This is the one that made the rounds in mid-2025. CVE-2025-54068 was a critical (CVSS v4 score of 9.2) remote code execution flaw affecting Livewire v3 from 3.0.0-beta.1 through 3.6.3, fixed in 3.6.4.
It was discovered and reported by Rémi Matasse and Pierre Martin of Synacktiv, who reported it to the Livewire team on 12 June 2025 and later published a detailed technical breakdown. Most of the Laravel community first heard about it through Stephen Rees-Carter's Securing Laravel notice, published the day after the patch, which described it as "a rather sneaky vulnerability that can be disastrously effective."
The mechanism is a neat illustration of everything above. The attacker crafted the updates portion of a Livewire request to smuggle in malicious data during the hydration step, forcing a property into a type it was never meant to hold and abusing Livewire's internal "synthesizers" to instantiate objects and ultimately run code. The reason the snapshot checksum didn't stop it is exactly the point made earlier: the malicious data rode in through the separate, unsigned updates payload rather than the checksummed snapshot. No authentication and no user interaction were required, only that a vulnerable component be mounted.
It was serious enough that in March 2026, CISA added it to its Known Exploited Vulnerabilities catalog and gave US federal agencies until early April to remediate it. A CVE only lands there when there's reliable evidence of real-world exploitation, so this was not theoretical.
CVE-2024-47823: Remote Code Execution via File Uploads
The earlier one, from October 2024, is a textbook upload bug. In Livewire before v2.12.7 and v3.5.2, the file extension of an uploaded file was guessed from its MIME type, and the actual extension in the filename wasn't validated. An attacker could upload a file with a legitimate MIME type (say image/png) but a .php extension, and if the app stored it under its original filename on a public, PHP-executing disk, that became remote code execution. It was rated High, classified as improper input validation, and fixed in 2.12.7 and 3.5.2.
This is precisely why the upload advice above matters: validate the real file, and let store() name it.
Keeping Livewire Patched
Both of those were fixed in small point releases, which means the cost of staying safe was tiny for teams that kept up to date. The practical routine is short.
Check which version you actually have:
1composer show livewire/livewire
Plenty of packages pull Livewire in indirectly (Filament, Laravel Pulse, various Statamic add-ons), so check the dependency tree to see what's really installed and why:
1composer why livewire/livewire --tree
Let Composer's auditor compare your installed packages against known advisories:
1composer audit
And update when you're behind:
1composer update livewire/livewire
I'd run composer audit in CI regardless of Livewire. It's a cheap way to catch a known vulnerability in any dependency before it reaches production, rather than reading about it after the fact.
Summary
Livewire security comes down to one habit: remember that the client controls everything it can see, and it can see more than you think. From there:
- Validate public properties with
#[Validate]and$this->validate(). - Lock server-only properties with
#[Locked]. - Authorise every action server-side with
$this->authorize(), and treat action parameters as untrusted. - Keep internal methods
protectedorprivateso they can't be called from the client. - Persist custom auth middleware with
Livewire::addPersistentMiddleware(). - Validate uploads and let
store()generate filenames. - Keep secrets out of public properties, since they're sent to the browser.
- Rate-limit sensitive actions with Laravel's
RateLimiter. - Stay patched and run
composer audit.
Livewire 4 is a great release, but it doesn't change any of this. The framework gives you solid tools, and the two Livewire 3 vulnerabilities were both fixed promptly, but the day-to-day security of your components is still down to how you write them.
Want a second opinion on the security of your Livewire or Laravel application? I work with Laravel and Statamic every day and can help you audit and harden your components. Get in touch to discuss your project.
Syntax highlighting by Torchlight
More articles
Antlers vs Blade in Statamic: Which Should You Use in 2026?
Statamic lets you pick between two templating engines: Antlers, the built-in CMS-first language, and Blade, the same engine you already know from Laravel. This guide walks through the real differences, what each one is good at, and how to mix them when it makes sense.
Read articleHow to Fix "Route [login] Not Defined" Error in Laravel 13 (2026 Guide)
The "Route [login] not defined" error in Laravel almost always means the auth middleware tried to redirect a guest to a login route that does not exist. This guide covers all the common causes and how to fix them in Laravel 13, including the new redirectGuestsTo middleware method, starter kits, API-only apps, and Sanctum SPA authentication.
Read article