Skip to content

Missing authorization on product variant DeleteAction/DeleteBulkAction in Form\Variants component: user with edit_products but without delete_product_variants can delete any product variant

High
mckenziearts published GHSA-93v2-gcw2-vfwc Jun 29, 2026

Package

composer shopper/framework (Composer)

Affected versions

< 2.10.0

Patched versions

2.10.0

Description

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.phpDeleteAction::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

  1. Attacker authenticates with edit_products permission (content-editor role — no delete_product_variants).
  2. Visits /shopper/products/{id}/editProduct/Edit.php::mount() passes (edit_products present) → receives signed Livewire snapshot for Form\Variants component.
  3. Calls callTableAction('delete', $variantId) or callTableBulkAction('delete', $variantIds) on the Livewire endpoint.
  4. 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.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H

CVE ID

CVE-2026-62989

Weaknesses

Improper Authorization

The product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action. Learn more on MITRE.

Missing Authorization

The product does not perform an authorization check when an actor attempts to access a resource or perform an action. Learn more on MITRE.

Credits