PT-2026-90542 · Packagist · Shopper/Framework

CVE-2026-56826

·

Publicado

2026-09-11

·

Atualizado

2026-09-11

CVSS v3.1

5.4

Média

VetorAV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L

Summary

Four Livewire components in the Settings area expose destructive Filament actions (delete / edit) that perform no server-side authorization. Any authenticated user who can reach the Settings pages — i.e. holding only the coarse access setting permission, without being an admin and without any delete */edit * permission — can delete tax zones, tax rates, shipping zones, and carrier (shipping-rate) options by invoking the component action directly over the Livewire endpoint.
These records sit on the storefront checkout path, so deleting them breaks shipping-rate calculation, removes region-scoped payment methods, and corrupts tax resolution at checkout.
This is inconsistent with the rest of the admin, where destructive actions are gated by granular permissions (e.g. Settings/Locations/Index uses ->authorize('delete inventories'), and Order/Detail gates mutating actions with edit orders).

Affected components

ComponentFileUnauthorized action
SettingsZonesZoneShippingOptionspackages/admin/src/Livewire/Components/Settings/Zones/ZoneShippingOptions.php:47deleteCarrierOption::query()->find($arguments['id'])->delete() (id is client-supplied)
SettingsZonesDetailpackages/admin/src/Livewire/Components/Settings/Zones/Detail.php:46deleteDeleteAction on the bound Zone
SettingsTaxesDetailpackages/admin/src/Livewire/Components/Settings/Taxes/Detail.php:42deleteDeleteAction on the bound TaxZone
SettingsTaxesTaxRatespackages/admin/src/Livewire/Components/Settings/Taxes/TaxRates.php:97deleteDeleteAction on a TaxRate
Each file contains zero authorize calls, and the actions declare neither ->authorize() nor an enforced ->visible() guard.

Details

The Settings pages mount these as child Livewire components. The parent page authorizes access setting (e.g. Pages/Settings/Taxes.php:29), but the child components do not re-check authorization, and their destructive actions carry no ->authorize(). Because each Livewire component handles its own /livewire/update requests, the action executes purely on the page-level access setting gate — there is no per-resource permission, and delete zones / delete taxes permissions are never even generated by the seeder (packages/admin/database/seeders/PermissionsTableSeeder.php).
ZoneShippingOptions::deleteAction() is the clearest case — it deletes by an id taken straight from the client action arguments with no scoping and no permission check:
php
// packages/admin/src/Livewire/Components/Settings/Zones/ZoneShippingOptions.php
public function deleteAction(): Action
{
  return Action::make('delete')
    ->requiresConfirmation()
    // ... no ->authorize(), no ->visible()
    ->action(function (array $arguments): void {
      CarrierOption::query()->find($arguments['id'])->delete();  // client-controlled id
      // ...
    });
}

Proof of Concept

Confirmed with the project's own test harness (Pest + Orchestra Testbench, SQLite) — the real Livewire/Filament code path, executed as a non-admin user holding only access setting.
php
use LivewireLivewire;
use ShopperCoreModels{CarrierOption, Zone};
use ShopperLivewireComponentsSettingsZonesZoneShippingOptions;
use TestsCoreStubsUser;

uses(TestsAdminTestCase::class);

it('low-priv access setting user deletes a CarrierOption with no authorization', function (): void {
  $attacker = User::factory()->create();
  $attacker->givePermissionTo('access setting');     // NOT admin, NO delete * permission
  $this->actingAs($attacker, config('shopper.auth.guard'));

  $zone  = Zone::factory()->create();
  $option = CarrierOption::factory()->create(['zone id' => $zone->id]);

  Livewire::test(ZoneShippingOptions::class, ['selectedZoneId' => $zone->id])
    ->callAction('delete', arguments: ['id' => $option->id]);

  expect(CarrierOption::query()->find($option->id))->toBeNull();  // deleted -> vulnerable
});
Result:
Attacker: isAdmin()=false, can('access setting')=true, can('delete zones')=false, can('edit zones')=false
[BEFORE] CarrierOption count = 1 (target #1 'DHL Express' exists = YES)
[ATTACK] callAction('delete', id=1) on ZoneShippingOptions
[AFTER ] CarrierOption count = 0 (target #1 exists = NO -> deleted)

PASS 3 passed (11 assertions)
 ✓ CONTROL — Order/Detail::markPaid is correctly hidden without edit orders (harness enforces declared authz)
 ✓ a CarrierOption is deleted by the low-priv user
 ✓ a shipping Zone is deleted by the low-priv user
The CONTROL case rules out a false positive: the same harness correctly denies Order/Detail::markPaid for a user lacking edit orders, proving authorization is enforced when a component declares it — these four components simply declare none.

Impact

A low-privileged staff member (or a compromised low-privileged account) can sabotage the storefront's checkout/revenue path without any delete permission:
  • Delete a CarrierOption → that shipping rate disappears from checkout for the zone.
  • Delete a Zone → removes the country → carrier/payment-method/currency mapping; customers shipping to those countries lose all shipping and payment options (CarrierRateService::getRatesForZone / getManualRates read these directly).
  • Delete a TaxZone / TaxRateTaxCalculator::resolveZone() can no longer resolve the zone, corrupting tax calculation at checkout.
Net effect: integrity and availability damage to live commerce configuration, performed by a principal who was never granted that authority (least-privilege violation).

Secondary issue found while reproducing

ZonesDetail::deleteAction()->after() calls $this->reset('zone'), but zone is a #[Computed] method (not a property), so it throws ReflectionException after the row is deleted. Worth fixing alongside the authorization gap.

Suggested remediation

Add an authorization check to each action, and ideally a mount() guard on each child component, matching the pattern already used in Settings/Locations/Index.php and Team/RolePermission.php:
php
public function deleteAction(): Action
{
  return Action::make('delete')
    ->authorize('access setting')  // or a new granular delete zones / delete taxes permission
    ->requiresConfirmation()
    // ...
}
Apply to the delete (and edit) actions in all four components. Consider also generating granular * zones / * taxes permissions so settings access can follow least privilege, and fix the $this->reset('zone') call in ZonesDetail.

Correção

Missing Authorization

Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾

Enumeração de Fraquezas

Identificadores relacionados

CVE-2026-56826
GHSA-F7H9-QV4X-9X57

Produtos afetados

Shopper/Framework