skip to content

Payload Security

Every public Livewire method is a callable endpoint and every public property is editable by the client, guarded by a snapshot checksum and #[Locked]. Interviewers probe what forged requests reach.

on this pageshow

explore

questions

6

In a Livewire component, which methods can the browser call, and why must their arguments be treated as untrusted input?

level: juniorimportance: must knowfreq 55%

answer

  1. every public method is an endpoint
  2. render() and lifecycle hooks are blocked
  3. #[Computed] methods refuse direct calls
  4. protected or private for internal helpers
  5. MethodNotFoundException, a bare 419 in production

basics

~20 s

The browser can call any public, non-static method your Livewire component class declares, referenced in a template or not, except render(), lifecycle hooks and #[Computed] methods. Arguments arrive in an editable JSON request, so authorize and validate them.

solid answer

~30 s

Livewire turns every public method declared on your component class, not on the base `Livewire\Component`, into an action: the browser posts a method name and parameters to the update endpoint and Livewire calls it. `wire:click="approve({{ $payslip->id }})"` is only a suggestion; anyone can change the id in devtools, or call a public method no button shows. Livewire refuses `render()`, lifecycle hooks such as `mount()` and `updated*()`, and `#[Computed]` methods, but public methods pulled in from traits you use are reachable. So internal helpers go `protected` or `private`, and every action authorizes and validates its own arguments before it touches the database.

code

php · 21 lines
php
<?php

use App\Models\Payslip;
use Livewire\Component;

class PayslipList extends Component
{
    public function approve(int $payslipId): void
    {
        $payslip = Payslip::findOrFail($payslipId);

        $this->authorize('approve', $payslip); // runs before any write

        $this->markApproved($payslip);
    }

    protected function markApproved(Payslip $payslip): void
    {
        $payslip->update(['approved_at' => now()]);
    }
}

go deeper

for a junior

Remember one rule: every public method on a Livewire component is callable from the browser with any arguments, so helpers belong in protected or private methods.

for a middle

Explain the call path: method name and params in the update payload, the public-methods allowlist, and which names Livewire blocks, such as render, lifecycle hooks and computed methods.

for a senior

Show how you audit a component: list every public method including trait methods, confirm each authorizes its own arguments, and catch refactors that split the check from the write.

for a principal

Frame visibility as the component's API surface and argue for team conventions, such as review checklists or static rules, that treat each public Livewire method like a controller route.

## What "public" means in a Livewire component A **Livewire component** is a PHP class, or the class part of a single-file component in Livewire 4, whose state lives in the browser between requests. When a user clicks a button bound with `wire:click`, the browser sends a request to Livewire's single **update endpoint**. The payload carries the component's signed snapshot, any property updates, and a list of **calls**: each is just a method name and an array of parameters. On the server, Livewire rebuilds the component and, for each call, checks the method name against the list of **public, non-static methods declared by your subclass**. Methods declared on `Livewire\Component` itself (and the traits it uses) are excluded; methods on your class, or on traits your class uses, are included. If the name is on the list, Livewire invokes it with the parameters exactly as the browser sent them. Two consequences follow: - A method is callable whether or not any template references it. Hiding the button hides nothing. - Parameters are whatever the client typed. `wire:click="approve(42)"` rendered by your Blade view can be replayed as `approve(43)` with no special tooling. ## What Livewire refuses to call | Method | What happens if the browser names it | |---|---| | `protected` / `private` method | Not on the list: `MethodNotFoundException` ("Public method [x] not found") | | `render()` | Removed from the callable list: same exception | | `mount()`, `boot()`, `hydrate*()`, `updated*()`, `rendering()` and the other hooks | `DirectlyCallingLifecycleHooksNotAllowedException` | | a `#[Computed]` method | `CannotCallComputedDirectlyException` | | a method from a trait your component uses | **Callable**, if it is public | Several of these exceptions, `MethodNotFoundException` among them, render as an empty **419** response when `app.debug` is false and as a full error page in debug mode. ## The payroll example On a payroll approval screen, a list shows each payslip with an Approve button: ```php public function approve(int $payslipId) { // INSECURE: any id the browser sends is approved Payslip::findOrFail($payslipId)->update(['approved_at' => now()]); } ``` A clerk who can see only their own department can call `approve` with another department's id. The fix belongs inside the action: load the record, authorize it against the current user, then act. Livewire offers the `#[Authorize]` attribute or the `authorize()` helper the base component inherits for that check; which rule to write is your policy's business. A subtler leak is the refactor: ```php public function approve(int $id) { $this->authorizeApproval($id); $this->markApproved($id); } public function markApproved(int $id) { /* no check */ } ``` `markApproved` is public, so the browser can skip `approve` and call it directly. Making it `protected` removes it from the callable list. ## Type-hinted parameters narrow the input, they do not authorize it Livewire invokes actions through its implicit binding helper, so a parameter typed as a model behaves like route model binding: `approve(Payslip $payslip)` turns the id the browser sent into a `Payslip` through `resolveRouteBinding()`, and a missing row throws `ModelNotFoundException`. Scalar types coerce or reject malformed values. Both are useful, and neither answers the real question: the bound payslip exists, but does it belong to a department this clerk may approve? Binding proves the record is real; only an authorization check proves the user may touch it. Treat the parameter list like a route's URI segments: typed, bound, and still attacker-chosen. ## Why Livewire does not guess Livewire cannot know which public methods you meant for the browser: templates are rendered at runtime, and Alpine code can call any action through `$wire`. So the rule is structural: **visibility is the allowlist**. The same thinking applies to public properties, which the client may update unless you lock them. ## Rules of thumb 1. Treat each public method as a route: it needs its own authorization and validation. 2. Keep helpers, queries and side-effect methods `protected` or `private`. 3. Audit traits: a trait that adds a public `delete()` to your component adds an endpoint. 4. Never rely on the absence of a button, a disabled attribute or `@can` in the view as the guard; those only shape the UI. 5. Type-hint parameters (`int $payslipId`, or a model class) so malformed values fail early, but remember a well-formed id can still belong to someone else. The payload guards in `config/livewire.php` (`payload.max_calls`, 50 by default) limit how many calls one request may carry; they limit abuse volume, not access.

  • A teammate adds a trait with a public resetAll() method to a Livewire component. What changed?
    Livewire builds the callable list from public methods declared on your component class, and trait methods count as declared there. So `resetAll()` became an endpoint any user of that page can invoke with any parameters. Either make it `protected`, or give it the same authorization an action would need.
  • What does the browser see when it calls a protected Livewire method in production?
    Livewire throws `MethodNotFoundException`. With `app.debug` false it renders an empty 419 response instead of an error page, so nothing about the component's internals leaks; in debug mode Laravel shows the full exception. The method itself never runs.

saying these in an interview costs you the question

  • Only methods referenced by wire:click in the view can be called.
  • Hiding the button with @can is enough to protect the action.
  • Livewire validates action parameters against the values it rendered.
  • Public methods from a trait are not exposed to the browser.
  • Protected methods are callable as long as no template uses them.
open as a page

On a Livewire payroll screen, a public $userId set in mount() is edited in the browser before Save - why doesn't the snapshot checksum stop it, and what does?

level: seniorimportance: must knowfreq 50%

basics

~20 s

The checksum signs only the snapshot the server issued; property updates travel beside it by design, since that is how wire:model works. #[Locked], a model-typed property, or an authorization check in the save action stops the edit.

open as a page

What does Livewire's #[Authorize] attribute do on a component action, and how does it find the model to check?

level: middleimportance: should knowfreq 30%

basics

~10 s

#[Authorize('ability', 'argument')] runs a Gate check before a Livewire action executes and returns 403 on failure. The argument resolves as a class name, a type-hinted method parameter, or a component property.

open as a page

Why is a Livewire public property the wrong place for an API token or an employee's bank details, even with a checksummed snapshot?

level: middleimportance: should knowfreq 40%

basics

~20 s

Every Livewire public property is serialized into the page's wire:snapshot attribute and every update response as readable JSON. The checksum only proves the snapshot was not altered; it does not hide anything, so secrets must stay server-side.

open as a page

When a Livewire component keeps an Eloquent model in a public property, what goes into the snapshot and what happens on the next request?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Only the model's class, or its morph-map alias, and primary key go into the Livewire snapshot. On each later request Livewire re-queries the row with firstOrFail, so unsaved attribute changes, loaded relations and select() constraints are gone.

open as a page

Why do Livewire update requests re-run some of the page route's middleware, and when must you call Livewire::addPersistentMiddleware()?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Livewire updates hit one shared endpoint, so the page route's middleware would not run again. Livewire re-applies only middleware on its persistent list, such as auth and can; custom middleware needs Livewire::addPersistentMiddleware() in a service provider.

open as a page