{"id":"GHSA-243p-f3cv-c5wh","summary":"Shopper: Authorization bypass in Filament bulk actions allows browse-only staff to mass-delete attributes/tags and mass-toggle visibility of brands/categories/suppliers","details":"## Summary\n\nFive Filament `groupedBulkActions` blocks across the Shopper admin Livewire pages omit the `-\u003eauthorize(...)` permission gate, while their per-record sibling actions (and other Shopper Index pages such as `Pages/Settings/Currencies.php`, `Pages/Reviews/Index.php`, `Pages/Collection/Index.php`, and `Pages/Discount/Index.php`) correctly chain `-\u003eauthorize(...)`. Each affected page's `mount()` only requires the read-only `browse_*` permission, so a low-privilege staff user holding only the read permission can drive the bulk endpoint via the standard Livewire `callTableBulkAction` flow and execute state-mutating operations they were never granted. The vulnerability is the same class as GHSA-f946-9qp6-vgch and GHSA-j328-xmgp-j4q3 (read-only permission gating a write action), just on a different surface (Filament 4 `groupedBulkActions` rather than top-level Livewire methods).\n\nA staff user holding only `browse_attributes` can permanently delete every product attribute in the catalog (cascading break of every dependent product variant). A user holding only `browse_tags` can permanently delete every product tag. Users holding `browse_brands`, `browse_categories`, or `browse_suppliers` can flip the visibility (`is_enabled`) of every brand/category/supplier in bulk, sabotaging storefront catalog visibility.\n\nCVSS 3.1: `AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H` = 8.1 High. CWE-285 (Improper Authorization) and CWE-862 (Missing Authorization). The attacker has low privilege (browse-only staff role), no user interaction, network reachable.\n\n## Vulnerable components (paths relative to repo root)\n\nAll references are HEAD = commit `ac9a760` on `master` (the very commit that closed the previous wave of authorization-drift bugs from GHSA-j328-xmgp-j4q3).\n\n### 1) `packages/admin/src/Livewire/Pages/Attribute/Browse.php`\n\nMount at line 36–39 requires only `browse_attributes`.\n\n- Lines 106–122: `DeleteBulkAction::make()` has NO `-\u003eauthorize(...)` chain (the surrounding per-record `delete` action at lines 95–104 correctly does `-\u003eauthorize('delete_attributes')`).\n- Lines 123–138: `BulkAction::make('enabled')` has NO `-\u003eauthorize(...)`.\n- Lines 139–155: `BulkAction::make('disabled')` has NO `-\u003eauthorize(...)`.\n\nNet effect: a `browse_attributes`-only user can **delete every row in the attributes table**, and toggle `is_enabled` on every attribute in one request. Deleting an attribute cascades into every product variant that references it via the `attribute_product` pivot.\n\n### 2) `packages/admin/src/Livewire/Pages/Tag/Index.php`\n\nMount at line 39 requires only `browse_tags`.\n\n- Lines 96–108: `DeleteBulkAction::make()` has NO `-\u003eauthorize(...)` chain (the per-record `delete` action at lines 79–94 correctly does `-\u003eauthorize('delete_tags')`).\n\nNet effect: a `browse_tags`-only user can delete every `ProductTag` row.\n\n### 3) `packages/admin/src/Livewire/Pages/Brand/Index.php`\n\nMount at line 37–40 requires only `browse_brands`.\n\n- Lines 97–112: `BulkAction::make('enabled')` has NO `-\u003eauthorize(...)`.\n- Lines 113–129: `BulkAction::make('disabled')` has NO `-\u003eauthorize(...)`.\n\nNet effect: a `browse_brands`-only user can flip `is_enabled` on every brand. Disabling all brands removes them from the storefront catalog. The per-record edit/delete actions and the `DeleteBulkAction` at lines 130–148 are correctly `-\u003eauthorize(...)` gated — only the visibility bulk actions were missed.\n\n### 4) `packages/admin/src/Livewire/Pages/Category/Index.php`\n\nMount at line 38–41 requires only `browse_categories`.\n\n- Lines 102–117: `BulkAction::make('enabled')` has NO `-\u003eauthorize(...)`.\n- Lines 118–133: `BulkAction::make('disabled')` has NO `-\u003eauthorize(...)`.\n\nNet effect: a `browse_categories`-only user can flip `is_enabled` on every category. Same shape as Brand.\n\n### 5) `packages/admin/src/Livewire/Pages/Supplier/Index.php`\n\nMount at line 38 requires only `browse_suppliers`.\n\n- Lines 93–108: `BulkAction::make('enabled')` has NO `-\u003eauthorize(...)`.\n- Lines 109–125: `BulkAction::make('disabled')` has NO `-\u003eauthorize(...)`.\n\nNet effect: a `browse_suppliers`-only user can flip `is_enabled` on every supplier.\n\n## Reference comparison: places that ARE correctly gated\n\nFor reference, here is what the same pattern looks like in files that DID get the fix:\n\n- `packages/admin/src/Livewire/Pages/Settings/Currencies.php` lines 90–129: every `BulkAction` chains `-\u003eauthorize('access_setting')`.\n- `packages/admin/src/Livewire/Pages/Reviews/Index.php` lines 105–119: `DeleteBulkAction` chains `-\u003eauthorize('delete_reviews')`.\n- `packages/admin/src/Livewire/Pages/Collection/Index.php` lines 109–128: `DeleteBulkAction` chains `-\u003eauthorize('delete_collections')`.\n- `packages/admin/src/Livewire/Pages/Discount/Index.php` lines 126–145: `DeleteBulkAction` chains `-\u003eauthorize('delete_discounts')`.\n\nThe convention is established and applied elsewhere — these five files just missed it.\n\n## Proof of Concept\n\nThe attached file `tests/Admin/Livewire/Pages/Brand/AuthBypassPocTest.php` (added in this report) contains seven Pest tests, each acting as a `browse_*`-only staff user and invoking the bulk endpoint. All seven pass on master @ `ac9a760`:\n\n```\n   PASS  Tests\\Admin\\Livewire\\Pages\\Brand\\AuthBypassPocTest\n  ✓ it SHOPPER-2 PoC: read-only viewer can mass-DISABLE all brands via unguarded BulkAction\n  ✓ it SHOPPER-2 PoC: read-only viewer can mass-ENABLE all brands via unguarded BulkAction\n  ✓ it SHOPPER-2 PoC: read-only viewer can mass-DISABLE all categories via unguarded BulkAction\n  ✓ it SHOPPER-2 PoC: read-only viewer can mass-DISABLE all suppliers via unguarded BulkAction\n  ✓ it SHOPPER-2 PoC: read-only viewer can DELETE all attributes via unguarded DeleteBulkAction\n  ✓ it SHOPPER-2 PoC: read-only viewer can mass-DISABLE all attributes via unguarded BulkAction\n  ✓ it SHOPPER-2 PoC: browse_tags viewer can DELETE all product tags via unguarded DeleteBulkAction\n\n  Tests:    7 passed (32 assertions)\n```\n\nEach test seeds three records, signs in a user holding only the corresponding `browse_*` permission, calls `Livewire::test(\u003cPage\u003e::class)-\u003ecallTableBulkAction(...)`, and asserts the side effect (records flipped or deleted). For example, the attribute mass-delete test:\n\n```php\n$this-\u003eviewer = User::factory()-\u003ecreate();\n$this-\u003eviewer-\u003egivePermissionTo('browse_attributes');\n$this-\u003eactingAs($this-\u003eviewer);\n\nAttribute::factory()-\u003ecount(3)-\u003ecreate();\nexpect($this-\u003eviewer-\u003ecan('delete_attributes'))-\u003etoBeFalse();\n\nLivewire::test(AttributeBrowse::class)\n    -\u003ecallTableBulkAction(\\Filament\\Actions\\DeleteBulkAction::class, Attribute::pluck('id')-\u003etoArray())\n    -\u003eassertHasNoErrors();\n\nexpect(Attribute::count())-\u003etoBe(0);\n```\n\nThe call uses the same `callTableBulkAction` helper Shopper's own test suite uses everywhere, which in turn drives the same Livewire `update` payload the browser would emit — so this is a faithful HTTP-level reproduction.\n\n## Suggested fix\n\nAdd `-\u003eauthorize(\u003ccorrect_permission\u003e)` to each of the five vulnerable groups, mirroring the pattern already used elsewhere:\n\n```diff\n // Pages/Attribute/Browse.php\n -\u003egroupedBulkActions([\n     DeleteBulkAction::make()\n+        -\u003eauthorize('delete_attributes')\n         -\u003elabel(__('shopper::forms.actions.delete'))\n         -\u003erequiresConfirmation()\n         -\u003eaction(function (Collection $records): void { /* ... */ }),\n     BulkAction::make('enabled')\n+        -\u003eauthorize('edit_attributes')\n         -\u003elabel(__('shopper::forms.actions.enable'))\n         -\u003eaction(function (Collection $records): void { /* ... */ }),\n     BulkAction::make('disabled')\n+        -\u003eauthorize('edit_attributes')\n         -\u003elabel(__('shopper::forms.actions.disable'))\n         -\u003eaction(function (Collection $records): void { /* ... */ }),\n ])\n```\n\nApply the equivalent change to `Pages/Tag/Index.php` (`delete_tags`), `Pages/Brand/Index.php` (`edit_brands` for enable/disable), `Pages/Category/Index.php` (`edit_categories`), and `Pages/Supplier/Index.php` (`edit_suppliers`).\n\nA regression test for each file (acting as a `browse_*`-only user and expecting `assertHasErrors`/`AuthorizationException`) would lock in the fix, matching the regression tests added for #514.\n\n## Resources\n\n- Prior advisories of the same class (read-only permission gating a write action): GHSA-f946-9qp6-vgch, GHSA-j328-xmgp-j4q3 / GHSA-vw82-3966-f9mr.\n- Same-shape fix: commit `ac9a760` (PR #514). Five Filament bulk-action groups did not receive the corresponding `-\u003eauthorize(...)` chain.\n- CWE-285 Improper Authorization, CWE-862 Missing Authorization.\n\n### Credits\n\nReported by Vishal Shukla(@shukla304) using sechub.dev AI Agent\n\n### Support\n\nIf this disclosure was useful and userswould like to support continued open-source security research and responsible-disclosure work, they can sponsor at https://github.com/sponsors/therawdev — Shopper is thankful for those keeping open source safe.","aliases":["CVE-2026-56827"],"modified":"2026-09-11T21:00:06.351826501Z","published":"2026-09-11T20:47:17Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-09-11T20:47:17Z","nvd_published_at":null,"cwe_ids":["CWE-862"],"severity":"HIGH"},"references":[{"type":"WEB","url":"https://github.com/shopperlabs/shopper/security/advisories/GHSA-243p-f3cv-c5wh"},{"type":"PACKAGE","url":"https://github.com/shopperlabs/shopper"}],"affected":[{"package":{"name":"shopper/framework","ecosystem":"Packagist","purl":"pkg:composer/shopper/framework"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.9.2"}]}],"versions":["v2.0.0","v2.0.0-alpha","v2.0.0-beta","v2.0.0-beta10","v2.0.0-beta11","v2.0.0-beta12","v2.0.0-beta13","v2.0.0-beta14","v2.0.0-beta15","v2.0.0-beta16","v2.0.0-beta17","v2.0.0-beta18","v2.0.0-beta19","v2.0.0-beta2","v2.0.0-beta20","v2.0.0-beta21","v2.0.0-beta3","v2.0.0-beta4","v2.0.0-beta5","v2.0.0-beta6","v2.0.0-beta7","v2.0.0-beta8","v2.0.0-beta9","v2.0.1","v2.0.2","v2.0.3","v2.1.1","v2.1.2","v2.1.3","v2.1.4","v2.1.5","v2.1.6","v2.2","v2.2.1","v2.2.2","v2.2.3","v2.2.4","v2.2.5","v2.2.6","v2.2.7","v2.3","v2.3.1","v2.3.2","v2.3.3","v2.4.0","v2.4.1","v2.4.2","v2.4.3","v2.5.0","v2.5.1","v2.6.0","v2.6.1","v2.6.2","v2.6.3","v2.6.4","v2.7.0","v2.7.1","v2.7.2","v2.7.3","v2.8.0","v2.8.1","v2.9.0","v2.9.1"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-243p-f3cv-c5wh/GHSA-243p-f3cv-c5wh.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H"}]}