Overview

breaking shopper checkout from settings

July 2, 2026
5 min read

TL;DR

I got CVE-2026-56826 in Shopper.

This one was a missing function-level authorization bug in the Settings area.

A low-privileged authenticated user only needed the coarse:

access_setting

permission.

They did not need to be admin, and they did not need granular delete_* or edit_* permissions.

From there, they could invoke destructive Livewire / Filament component actions directly and delete commerce configuration used by checkout.

The affected package was:

shopper/framework

The bug

The issue lived in four Settings components.

The parent Settings pages checked whether the user could access settings at all, but the child Livewire components exposed destructive actions without their own server-side authorization.

The affected components were:

ComponentAction
Settings\Zones\ZoneShippingOptionsdelete carrier / shipping-rate options
Settings\Zones\Detaildelete a shipping zone
Settings\Taxes\Detaildelete a tax zone
Settings\Taxes\TaxRatesdelete a tax rate

The important part is that these actions did not declare an enforced ->authorize() guard.

So the authorization question became too broad:

can this user access settings?

when it should have been:

can this user delete this tax/zone/shipping configuration?

Those are not the same permission.

The clearest sink

The cleanest example was ZoneShippingOptions::deleteAction().

The action accepted an id from the Livewire action arguments and deleted the matching CarrierOption:

packages/admin/src/Livewire/Components/Settings/Zones/ZoneShippingOptions.php
public function deleteAction(): Action
{
return Action::make('delete')
->requiresConfirmation()
// no ->authorize(), no enforced ->visible()
->action(function (array $arguments): void {
CarrierOption::query()->find($arguments['id'])->delete();
});
}

That is a bad shape for a destructive action.

The id came from the client-side action arguments, and the server-side action did not verify whether the current user had authority to delete carrier options.

Confirmation UI does not fix that. If the Livewire endpoint accepts the action, the server still has to authorize it.

Why it worked

The Settings pages mounted these child components after the broader settings access check.

That meant the attacker had enough permission to reach the area:

access_setting = true

But they did not have the destructive permissions that should have protected these actions:

delete_zones = false
edit_zones = false
delete_taxes = false
edit_taxes = false

The problem was that the vulnerable components never asked those questions.

The advisory also notes that this was inconsistent with other areas of the admin. Some existing components already used explicit authorization on mutating actions, for example order mutation paths guarded by edit_orders.

That made the bug easier to trust: the framework could enforce action authorization when the component declared it. These components simply did not declare it.

Runtime proof

The reproducer used the project’s own test harness with Pest, Orchestra Testbench, SQLite, Livewire, and Filament.

The attacker was:

  • authenticated
  • non-admin
  • granted only access_setting
  • not granted delete or edit permissions for the target resource

The core proof looked like this:

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');
$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();
});

The result was direct:

Attacker: isAdmin()=false, can('access_setting')=true,
can('delete_zones')=false, can('edit_zones')=false
[BEFORE] CarrierOption count = 1
[ATTACK] callAction('delete', id=1) on ZoneShippingOptions
[AFTER ] CarrierOption count = 0

So this was not just a UI visibility issue.

The row was actually deleted.

Negative control

The useful negative control was an unrelated admin action that already had proper authorization.

The same harness correctly denied Order/Detail::markPaid for a user lacking:

edit_orders

That matters because it shows the harness was not bypassing authorization globally.

Authorization worked when a component declared it.

The vulnerable Settings components just had no function-level authorization on their destructive actions.

Impact

This was not about stealing customer data.

It was about damaging live commerce configuration.

A low-privileged staff account, or a compromised staff account with only Settings access, could sabotage checkout by deleting:

  • a CarrierOption, making a shipping rate disappear
  • a Zone, breaking country to carrier / payment-method / currency mapping
  • a TaxZone, breaking tax-zone resolution
  • a TaxRate, corrupting tax calculation

Those records sit directly on the checkout path.

So the practical effect is integrity and availability damage:

shipping options disappear
payment methods can stop matching a region
tax calculation becomes wrong or incomplete
checkout becomes unreliable

That is enough to hurt a storefront even without confidentiality impact.

Secondary issue

While reproducing the bug, there was also a smaller issue in:

Zones\Detail::deleteAction()->after()

It called:

$this->reset('zone')

but zone was a #[Computed] method, not a property.

So the component could throw a ReflectionException after the row had already been deleted.

That was not the main security issue, but it was worth fixing near the same code path.

Fix direction

The fix direction is simple: destructive actions need server-side authorization at the action level.

For example:

public function deleteAction(): Action
{
return Action::make('delete')
->authorize('access_setting') // or better: granular delete_zones / delete_taxes
->requiresConfirmation();
}

The better version is to add granular permissions for the resources themselves, so access_setting does not become a catch-all permission for modifying commerce-critical configuration.

At minimum, each delete and edit action in the affected components needs an enforced authorization check.

Severity and versions

The public advisory lists:

  • CVE: CVE-2026-56826
  • GHSA: GHSA-f7h9-qv4x-9x57
  • Severity: Moderate
  • CVSS: 5.4
  • Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L
  • CWE: CWE-862
  • Affected: >= 2.0.0, < 2.9.2
  • Patched: 2.9.2

Reference