Summary
The Variants Livewire component (packages/admin/src/Livewire/Components/Products/Form/Variants.php) renders a Filament table of product variants and exposes two destructive actions — DeleteAction (single record) and DeleteBulkAction (multiple records) — with zero server-side authorization checks.
The parent page (Livewire/Pages/Product/Edit.php) gates access with $this->authorize('edit_products'). This is the only permission verified before a user can trigger variant deletion. The component itself contains no authorize(), can(), gate, or policy call.
Every other variant-mutating action correctly checks variant-specific permissions:
SlideOvers/UpdateVariant.php:65,150 → $this->authorize('edit_product_variants')
SlideOvers/GenerateVariants.php:40,49 → $this->authorize('edit_product_variants')
Components/Products/VariantStock.php:41 → ->authorize('edit_product_variants')
The expected permission for deletion (delete_product_variants) is seeded by Permission::generate('product_variants', 'products') in PermissionsTableSeeder.php but is never checked in the delete path.
Affected component
packages/admin/src/Livewire/Components/Products/Form/Variants.php — DeleteAction::make() (line ~117) and DeleteBulkAction::make() inline action (line ~123):
->recordActions([
DeleteAction::make() // NO ->authorize(), NO ->visible(can(...))
->icon(Untitledui::Trash03)
->iconButton()
->modalIcon(Untitledui::Trash03)
->successNotificationTitle(...),
])
->groupedBulkActions([
DeleteBulkAction::make() // NO ->authorize() anywhere
->action(function (Collection $records): void {
$records->each->delete(); // deletes without permission check
}),
])
Exploit path
- Attacker authenticates with
edit_products permission (content-editor role — no delete_product_variants).
- Visits
/shopper/products/{id}/edit → Product/Edit.php::mount() passes (edit_products present) → receives signed Livewire snapshot for Form\Variants component.
- Calls
callTableAction('delete', $variantId) or callTableBulkAction('delete', $variantIds) on the Livewire endpoint.
DeleteAction triggers $record->delete() / $records->each->delete() with zero authorization. All targeted variants are permanently deleted.
Proof of Concept
Pest test written against Shopper's own test infrastructure (Orchestra Testbench + SQLite), following the exact pattern of the 13 prior confirmed GHSAs:
$user = User::factory()->create();
$user->givePermissionTo('edit_products'); // ONLY — no delete_product_variants
actingAs($user);
$product = Product::factory()->create(['type' => ProductType::Variant]);
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
expect($user->can('delete_product_variants'))->toBeFalse();
Livewire::test(Variants::class, ['product' => $product])
->callTableAction('delete', $variant);
expect(ProductVariant::find($variant->id))->toBeNull(); // deleted without delete_product_variants
Test file: tests/Admin/Livewire/Components/Products/Form/VariantsDeleteAuthTest.php
Run with: vendor/bin/pest tests/Admin/Livewire/Components/Products/Form/VariantsDeleteAuthTest.php --configuration phpunit.sqlite.xml
Impact
Any admin user with edit_products (content editor) can permanently delete product variants without the delete_product_variants permission. For ProductType::Variant products this destroys the product's sellable catalogue, breaking storefront product pages, cart, and checkout.
Remediation
Add ->authorize('delete_product_variants') to both DeleteAction::make() and DeleteBulkAction::make() (and inside the bulk action callback) in packages/admin/src/Livewire/Components/Products/Form/Variants.php. Consistent with the pattern used throughout the codebase for variant-mutating actions.
Summary
The
VariantsLivewire component (packages/admin/src/Livewire/Components/Products/Form/Variants.php) renders a Filament table of product variants and exposes two destructive actions —DeleteAction(single record) andDeleteBulkAction(multiple records) — with zero server-side authorization checks.The parent page (
Livewire/Pages/Product/Edit.php) gates access with$this->authorize('edit_products'). This is the only permission verified before a user can trigger variant deletion. The component itself contains noauthorize(),can(), gate, or policy call.Every other variant-mutating action correctly checks variant-specific permissions:
SlideOvers/UpdateVariant.php:65,150→$this->authorize('edit_product_variants')SlideOvers/GenerateVariants.php:40,49→$this->authorize('edit_product_variants')Components/Products/VariantStock.php:41→->authorize('edit_product_variants')The expected permission for deletion (
delete_product_variants) is seeded byPermission::generate('product_variants', 'products')inPermissionsTableSeeder.phpbut is never checked in the delete path.Affected component
packages/admin/src/Livewire/Components/Products/Form/Variants.php—DeleteAction::make()(line ~117) andDeleteBulkAction::make()inline action (line ~123):Exploit path
edit_productspermission (content-editor role — nodelete_product_variants)./shopper/products/{id}/edit→Product/Edit.php::mount()passes (edit_productspresent) → receives signed Livewire snapshot forForm\Variantscomponent.callTableAction('delete', $variantId)orcallTableBulkAction('delete', $variantIds)on the Livewire endpoint.DeleteActiontriggers$record->delete()/$records->each->delete()with zero authorization. All targeted variants are permanently deleted.Proof of Concept
Pest test written against Shopper's own test infrastructure (Orchestra Testbench + SQLite), following the exact pattern of the 13 prior confirmed GHSAs:
Test file:
tests/Admin/Livewire/Components/Products/Form/VariantsDeleteAuthTest.phpRun with:
vendor/bin/pest tests/Admin/Livewire/Components/Products/Form/VariantsDeleteAuthTest.php --configuration phpunit.sqlite.xmlImpact
Any admin user with
edit_products(content editor) can permanently delete product variants without thedelete_product_variantspermission. ForProductType::Variantproducts this destroys the product's sellable catalogue, breaking storefront product pages, cart, and checkout.Remediation
Add
->authorize('delete_product_variants')to bothDeleteAction::make()andDeleteBulkAction::make()(and inside the bulk action callback) inpackages/admin/src/Livewire/Components/Products/Form/Variants.php. Consistent with the pattern used throughout the codebase for variant-mutating actions.