- Package
- shopper/framework
- Fixed in
- 2.9.2
- Published
- Fri 11 Sep 2026, 22:28 Lisbon
- Installs on Packagist
- 18K
In plain words
An authenticated user with the access_setting permission can delete tax zones, tax rates, shipping zones and carrier shipping-rate options without being an admin or having delete permissions. Four Settings components lack server-side authorisation for these actions. Deleting these records can break shipping-rate calculation, remove regional payment options and corrupt tax calculation at checkout.
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| shopper/framework | >= 2.0.0, < 2.9.2 | 2.9.2 |
Am I affected?
In composer.lock, find shopper/framework and check whether its version is at least 2.0.0 but below 2.9.2; 2.9.2 is fixed. Check whether non-admin users have access_setting and can reach the Settings pages for shipping zones, carrier options, tax zones or tax rates. These users do not need granular delete_* or edit_* permissions to exploit the flaw.
What to do
Check which version you have, then update to a fixed release for your branch (2.9.2):
composer show shopper/framework | grep versions
composer update shopper/framework
If your version constraint in composer.json keeps you below a fixed release, raise it first. Whether your configuration is affected, and the exact version ranges, are in the advisory itself.
If you cannot update yet
The advisory recommends adding authorisation checks to the delete and edit actions in all four affected components, ideally with a mount() guard on each child component. It also suggests generating granular *_zones and *_taxes permissions so Settings access can follow least privilege.
The advisory's description
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
| Component | File | Unauthorized action |
|---|---|---|
Settings\Zones\ZoneShippingOptions |
packages/admin/src/Livewire/Components/Settings/Zones/ZoneShippingOptions.php:47 |
delete → CarrierOption::query()->find($arguments['id'])->delete() (id is client-supplied) |
Settings\Zones\Detail |
packages/admin/src/Livewire/Components/Settings/Zones/Detail.php:46 |
delete → DeleteAction on the bound Zone |
Settings\Taxes\Detail |
packages/admin/src/Livewire/Components/Settings/Taxes/Detail.php:42 |
delete → DeleteAction on the bound TaxZone |
Settings\Taxes\TaxRates |
packages/admin/src/Livewire/Components/Settings/Taxes/TaxRates.php:97 |
delete → DeleteAction 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:
// 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.
use Livewire\Livewire;
use Shopper\Core\Models\{CarrierOption, Zone};
use Shopper\Livewire\Components\Settings\Zones\ZoneShippingOptions;
use Tests\Core\Stubs\User;
uses(Tests\Admin\TestCase::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/getManualRatesread these directly). - Delete a
TaxZone/TaxRate→TaxCalculator::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
Zones\Detail::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:
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 Zones\Detail.
Weakness: CWE-862 Missing Authorization. Reported by baradika.
The advisory
- GitHub Advisory Database: GHSA-f7h9-qv4x-9x57, with who is affected and how
- OSV.dev: GHSA-f7h9-qv4x-9x57
- The week's digest, with the other advisories from 7 Sep 2026
- All advisories for shopper/framework