Security

Are you a visual learner?
Master Livewire with our in-depth screencasts
Watch now

It's important to make sure your Livewire apps are secure and don't expose any application vulnerabilities. Livewire has internal security features to handle many cases, however, there are times when it's up to your application code to keep your components secure.

Authorizing action parameters

Livewire actions are extremely powerful, however, any parameters passed to Livewire actions are mutable on the client and should be treated as un-trusted user input.

Arguably the most common security pitfall in Livewire is failing to validate and authorize Livewire action calls before persisting changes to the database.

Here is an example of an insecurity resulting from a lack of authorization:

<?php
 
use App\Models\Post;
use Livewire\Component;
 
class ShowPost extends Component
{
// ...
 
public function delete($id)
{
// INSECURE!
 
$post = Post::find($id);
 
$post->delete();
}
}
<button wire:click="delete({{ $post->id }})">Delete Post</button>

The reason the above example is insecure is that wire:click="delete(...)" can be modified in the browser to pass ANY post ID a malicious user wishes.

Action parameters (like $id in this case) should be treated the same as any untrusted input from the browser.

Therefore, to keep this application secure and prevent a user from deleting another user's post, we must add authorization to the delete() action.

First, let's create a Laravel Policy for the Post model by running the following command:

php artisan make:policy PostPolicy --model=Post

After running the above command, a new Policy will be created inside app/Policies/PostPolicy.php. We can then update its contents with a delete method like so:

<?php
 
namespace App\Policies;
 
use App\Models\Post;
use App\Models\User;
 
class PostPolicy
{
/**
* Determine if the given post can be deleted by the user.
*/
public function delete(?User $user, Post $post): bool
{
return $user?->id === $post->user_id;
}
}

Before you can use the new Policy, you need to register it inside app\Providers\AuthServiceProvider.php:

// ...
 
protected $policies = [
Post::class => PostPolicy::class,
];
 
// ...

Now, we can use the $this->authorize() method from the Livewire component to ensure the user owns the post before deleting it:

public function delete($id)
{
$post = Post::find($id);
 
// If the user doesn't own the post,
// an AuthorizationException will be thrown...
$this->authorize('delete', $post);
 
$post->delete();
}

Further reading:

Authorizing public properties

Similar to action parameters, public properties in Livewire should be treated as un-trusted input from the user.

Here is the same example from above about deleting a post, written insecurely in a different manner:

<?php
 
use App\Models\Post;
use Livewire\Component;
 
class ShowPost extends Component
{
public $postId;
 
public function mount($postId)
{
$this->postId = $postId;
}
 
public function delete()
{
// INSECURE!
 
$post = Post::find($this->postId);
 
$post->delete();
}
}
<button wire:click="delete">Delete Post</button>

As you can see, instead of passing the $postId as a parameter to the delete method from wire:click, we are storing it as a public property on the Livewire component.

The problem with this approach is that any malicious user can inject a custom element onto the page such as:

<input type="text" wire:model="postId">

This would allow them to freely modify the $postId before pressing "Delete Post". Because the delete action doesn't authorize the value of $postId, the user can now delete any post in the database, whether they own it or not.

To protect against this risk, there are two possible solutions:

Using model properties

When setting public properties, Livewire treats models differently than plain values such as strings and integers. Because of this, if we instead store the entire post model as a property on the component, Livewire will ensure the ID is never tampered with.

Here is an example of storing a $post property instead of a simple $postId property:

<?php
 
use App\Models\Post;
use Livewire\Component;
 
class ShowPost extends Component
{
public Post $post;
 
public function mount($postId)
{
$this->post = Post::find($postId);
}
 
public function delete()
{
$this->post->delete();
}
}
<button wire:click="delete">Delete Post</button>

This component is now secured because there is no way for a malicious user to change the $post property to a different Eloquent model.

Locking the property

Another way to prevent properties from being set to unwanted values is to use locked properties. Locking properties is done by applying the #[Locked] attribute. Now if users attempt to tamper with this value an error will be thrown.

Note that properties with the Locked attribute can still be changed in the back-end, so care still needs to taken that untrusted user input is not passed to the property in your own Livewire functions.

<?php
 
use App\Models\Post;
use Livewire\Component;
use Livewire\Attributes\Locked;
 
class ShowPost extends Component
{
#[Locked]
public $postId;
 
public function mount($postId)
{
$this->postId = $postId;
}
 
public function delete()
{
$post = Post::find($this->postId);
 
$post->delete();
}
}

Authorizing the property

If using a model property is undesired in your scenario, you can of course fall-back to manually authorizing the deletion of the post inside the delete action:

<?php
 
use App\Models\Post;
use Livewire\Component;
 
class ShowPost extends Component
{
public $postId;
 
public function mount($postId)
{
$this->postId = $postId;
}
 
public function delete()
{
$post = Post::find($this->postId);
 
$this->authorize('delete', $post);
 
$post->delete();
}
}
<button wire:click="delete">Delete Post</button>

Now, even though a malicious user can still freely modify the value of $postId, when the delete action is called, $this->authorize() will throw an AuthorizationException if the user does not own the post.

Further reading:

Middleware

When a Livewire component is loaded on a page containing route-level Authorization Middleware, like so:

Route::get('/post/{post}', App\Livewire\UpdatePost::class)
->middleware('can:update,post');

Livewire will ensure those middlewares are re-applied to subsequent Livewire network requests. This is referred to as "Persistent Middleware" in Livewire's core.

Persistent middleware protects you from scenarios where the authorization rules or user permissions have changed after the initial page-load.

Here's a more in-depth example of such a scenario:

Route::get('/post/{post}', App\Livewire\UpdatePost::class)
->middleware('can:update,post');
<?php
 
use App\Models\Post;
use Livewire\Component;
use Livewire\Attributes\Validate;
 
class UpdatePost extends Component
{
public Post $post;
 
#[Validate('required|min:5')]
public $title = '';
 
public $content = '';
 
public function mount()
{
$this->title = $this->post->title;
$this->content = $this->post->content;
}
 
public function update()
{
$this->post->update([
'title' => $this->title,
'content' => $this->content,
]);
}
}

As you can see, the can:update,post middleware is applied at the route-level. This means that a user who doesn't have permission to update a post cannot view the page.

However, consider a scenario where a user:

  • Loads the page
  • Loses permission to update after the page loads
  • Tries updating the post after losing permission

Because Livewire has already successfully loaded the page you might ask yourself: "When Livewire makes a subsequent request to update the post, will the can:update,post middleware be re-applied? Or instead, will the un-authorized user be able to update the post successfully?"

Because Livewire has internal mechanisms to re-apply middleware from the original endpoint, you are protected in this scenario.

Configuring persistent middleware

By default, Livewire persists the following middleware across network requests:

\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Laravel\Jetstream\Http\Middleware\AuthenticateSession::class,
\Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\RedirectIfAuthenticated::class,
\Illuminate\Auth\Middleware\Authenticate::class,
\Illuminate\Auth\Middleware\Authorize::class,
\App\Http\Middleware\Authenticate::class,

If any of the above middlewares are applied to the initial page-load, they will be persisted (re-applied) to any future network requests.

However, if you are applying a custom middleware from your application the initial page-load, and want it persisted between Livewire requests, you will need to add it to this list from a Service Provider in your app like so:

<?php
 
namespace App\Providers;
 
use Illuminate\Support\ServiceProvider;
use Livewire;
 
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Livewire::addPersistentMiddleware([
App\Http\Middleware\EnsureUserHasRole::class,
]);
}
}

If a Livewire component is loaded on a page that uses the EnsureUserHasRole middleware from your application, it will now be persisted and re-applied to any future network requests to that Livewire component.

Middleware arguments are not supported

Livewire currently doesn't support middleware arguments for persistent middleware definitions.

// Bad...
Livewire::addPersistentMiddleware(AuthorizeResource::class.':admin');
 
// Good...
Livewire::addPersistentMiddleware(AuthorizeResource::class);

Applying global Livewire middleware

Alternatively, if you wish to apply specific middleware to every single Livewire update network request, you can do so by registering your own Livewire update route with any middleware you wish:

Livewire::setUpdateRoute(function ($handle) {
return Route::post('/livewire/update', $handle)
->middleware(App\Http\Middleware\LocalizeViewPaths::class);
});

Any Livewire AJAX/fetch requests made to the server will use the above endpoint and apply the LocalizeViewPaths middleware before handling the component update.

Learn more about customizing the update route on the Installation page.

Snapshot checksums

Between every Livewire request, a snapshot is taken of the Livewire component and sent to the browser. This snapshot is used to re-build the component during the next server round-trip.

Learn more about Livewire snapshots in the Hydration documentation.

Because fetch requests can be intercepted and tampered with in a browser, Livewire generates a "checksum" of each snapshot to go along with it.

This checksum is then used on the next network request to verify that the snapshot hasn't changed in any way.

If Livewire finds a checksum mismatch, it will throw a CorruptComponentPayloadException and the request will fail.

This protects against any form of malicious tampering that would otherwise result in granting users the ability to execute or modify unrelated code.