Data security and performance in Laravel Livewire components

Recently I stumbled upon an issue with Laravel Livewire which happened to be both a security and a performance issue.

Here is a simplified version of an E-Commerce product page:

<h1>{{ $product->name}}</h1>

@auth('web')
    <div>
        @livewire('sales.product-price', ['product' => $product])
    </div>
@endauth

And now a simplified version of the component code:

class ProductPrice extends Component
{
    public Product $product;

    ...
}

The resulting HTML most probably contains all the current $product model's data attributes and relationships, thus exposing a lot of internals which could possibly lead to some security issues.

That security issue is currently discussed here, but to this day no livewire-core solution exists.

One could think of passing only an external product id which would hide all the internals. For example:

<h1>{{ $product->name}}</h1>

@auth('web')
    <div>
        @livewire('sales.product-price', ['productUuid' => $product->uuid])
    </div>
@endauth

And then the components code would look like this:

class ProductPrice extends Component
{
    public string $productUuid;
    protected ?Product $product = null;
    
    protected function getProduct(): Product {
        return $this->product ??= Product::whereUuid($this->productUuid)->firstOrFail();
    }
}

That would take care of the security issue but would introduce a performance issue: the already loaded product would be loaded again within the component. And if additional relations are needed, those are loaded again from the database. Not good.

The solution I came up with is fairly simple: provide the component with the product model, set the $product property to protected, and reference it via the mount() method:

Still using only the product model for initial reference:

<h1>{{ $product->name}}</h1>

@auth('web')
    <div>
        @livewire('sales.product-price', ['product' => $product])
    </div>
@endauth

But the component code will handle the product different this time:

class ProductPrice extends Component
{
    public string $productUuid;
    protected ?Product $product = null;

    public function mount(Product $product) {
        $this->product = $product;
        $this->productUuid = $product->uuid;
    }
    
    protected function getProduct(): Product {
        return $this->product ??= Product::whereUuid($this->productUuid)->firstOrFail();
    }
}

That way the internals of the product model are not exposed, and for the initial loading of the component the already existing $product model is used. No additional performance issue. And for further requests within that component the $productUuid exists which gives away just enough information to load the $product again.

Show Comments