GHSA-f7h9-qv4x-9x57: SQL Injection
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 accesssetting 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('deleteinventories'), and Order/Detail gates mutating actions with editorders).
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 accesssetting (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 accesssetting gate — there is no per-resource permission, and deletezones / deletetaxes 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 accesssetting.
php 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 accesssetting user deletes a CarrierOption with no authorization', function (): void { $attacker = User::factory()->create(); $attacker->givePermissionTo('accesssetting'); // NOT admin, NO delete permission $this->actingAs($attacker, config('shopper.auth.guard'));
$zone = Zone::factory()->create(); $option = CarrierOption::factory()->create(['zoneid' => $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('accesssetting')=true, can('deletezones')=false, can('editzones')=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 editorders (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 editorders, 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 / 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:
php public function deleteAction(): Action { return Action::make('delete') ->authorize('accesssetting') // or a new granular deletezones / deletetaxes 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.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
composer/shopper/frameworkto a version that resolves this vulnerability.Fixed in 2.9.2 - Configuration
Update these Livewire components to enforce server-side authorization on every destructive Filament Action (delete and edit) instead of relying on the parent page’s coarse access_setting gate: packages/admin/src/Livewire/Components/Settings/Taxes/Detail.php (delete on bound TaxZone), packages/admin/src/Livewire/Components/Settings/Taxes/TaxRates.php (delete on bound TaxRate), packages/admin/src/Livewire/Components/Settings/Zones/Detail.php (delete on bound Zone), and packages/admin/src/Livewire/Components/Settings/Zones/ZoneShippingOptions.php (delete Action calling CarrierOption::query()->find($arguments['id'])->delete()).
Livewire/Filament Settings child components (4) server-side authorization for destructive actions (delete/edit) = Add per-action authorization checks to every delete (and edit) action, and guard mounts/child components (e.g., mount() guard) using the same authorization pattern as Settings/Locations/Index.php and Team/RolePermission.php - Configuration
Fix Zones\Detail::deleteAction()->after() which calls `$this->reset('zone')`; the material states `zone` is a `#[Computed]` method (not a property) and throws a ReflectionException after the row is deleted. Modify the reset logic to avoid resetting a computed method.
packages/admin/src/Livewire/Components/Settings/Zones/Detail.php reset('zone') call after delete = Fix so reset targets the correct state (not a #[Computed] method)
Event History
Frequently Asked Questions
Can an unauthenticated attacker exploit this issue?
No. Exploitation requires an authenticated user account that can reach the Settings pages through the coarse access_setting permission.
Do affected users need administrator or granular edit/delete permissions?
No. A user with access_setting can invoke the vulnerable Livewire actions directly even without administrator status or the relevant delete_* or edit_* permissions.
What operational impact can unauthorized deletion have?
Deleting tax zones, tax rates, shipping zones, or carrier options can break shipping-rate calculation, remove region-scoped payment methods, and corrupt tax resolution during checkout.