CVE-2026-56826: SQL Injection

Published Sep 11, 2026
·
Updated

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

1 affected componentFixes available
composer/shopper/framework>=2.0.0<2.9.2
2.9.2

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade composer/shopper/framework to a version that resolves this vulnerability.

    Fixed in 2.9.2
  2. Configuration

    In each of the four Settings-area Livewire child components, add server-side authorization to the destructive Filament actions (at least the `delete` action; ideally also `edit`). Do not rely on the parent page’s coarse `access_setting` gate.

    packages/admin/src/Livewire/Components/Settings/Zones/ZoneShippingOptions.php authorize = Add server-side authorization checks to all destructive actions (delete/edit)
  3. Configuration

    Update `ZoneShippingOptions::deleteAction()` so it does NOT delete using a client-controlled `id` argument without authorization. Perform `->authorize(...)` for a delete-specific permission (and/or enforce least privilege such as `delete_zones`), and delete only the resolved model/resource for the currently selected zone rather than `CarrierOption::query()->find($arguments['id'])->delete()`.

    packages/admin/src/Livewire/Components/Settings/Zones/ZoneShippingOptions.php deleteAction authorization = Require granular permission for delete (e.g., `delete_zones`) and scope deletion to the bound resource
  4. Configuration

    Fix the `Zones\Detail::deleteAction()->after()` handler that calls `$this->reset('zone')`. Since `zone` is a `#[Computed]` method (not a property), the reset throws a `ReflectionException` after deletion; adjust the reset logic to target the correct state or remove the invalid reset.

    packages/admin/src/Livewire/Components/Settings/Zones/Detail.php $this->reset('zone') = Fix reset target so it addresses the computed state correctly (avoid resetting a `#[Computed]` method)

Event History

Sep 11, 2026
Advisory Published
via GitHub·09:28 PM
Data Sourced
via GitHub·09:28 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which users are exposed to this issue?

Any authenticated user who can access the Settings pages with the coarse access_setting permission is in scope. Admin status and granular delete_* or edit_* permissions are not required.

2

What does an attacker need to do to exploit it?

The attacker needs an authenticated account with access to the Settings area and can invoke the affected Livewire component action directly through the Livewire endpoint. No user interaction is required.

3

What can be affected if exploitation succeeds?

An attacker can delete tax zones, tax rates, shipping zones, and carrier shipping-rate options. This can disrupt shipping-rate calculation, remove region-scoped payment methods, and corrupt tax resolution during checkout.

4

Which application areas should be reviewed first?

Review the Settings Livewire components for zones and shipping options, particularly destructive delete and edit actions. Confirm that these actions enforce server-side granular authorization rather than relying only on access_setting access.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203