Shopper: Authorization bypass in Filament bulk actions allows browse-only staff to mass-delete attributes/tags and mass-toggle visibility of brands/categories/suppliers
Published September 11, 2026Updated September 11, 2026Source: osv
Summary
## Summary
Five Filament `groupedBulkActions` blocks across the Shopper admin Livewire pages omit the `->authorize(...)` 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 `->authorize(...)`. 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).
A 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.
CVSS 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.
## Vulnerable components (paths relative to repo root)
All references are HEAD = commit `ac9a760` on `master` (the very commit that closed the previous wave of authorization-drift bugs from GHSA-j328-xmgp-j4q3).
### 1) `packages/admin/src/Livewire/Pages/Attribute/Browse.php`
Mount at line 36–39 requires only `browse_attributes`.
- Lines 106–122: `DeleteBulkAction::make()` has NO `->authorize(...)` chain (the surrounding per-record `delete` action at lines 95–104 correctly does `->authorize('delete_attributes')`).
- Lines 123–138: `BulkAction::make('enabled')` has NO `->authorize(...)`.
- Lines 139–155: `BulkAction::make('disabled')` has NO `->authorize(...)`.
Net 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.
### 2) `packages/admin/src/Livewire/Pages/Tag/Index.php`
Mount at line 39 requires only `browse_tags`.
- Lines 96–108: `DeleteBulkAction::make()` has NO `->authorize(...)` chain (the per-record `delete` action at lines 79–94 correctly does `->authorize('delete_tags')`).
Net effect: a `browse_tags`-only user can delete every `ProductTag` row.
### 3) `packages/admin/src/Livewire/Pages/Brand/Index.php`
Mount at line 37–40 requires only `browse_brands`.
- Lines 97–112: `BulkAction::make('enabled')` has NO `->authorize(...)`.
- Lines 113–129: `BulkAction::make('disabled')` has NO `->authorize(...)`.
Net 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 `->authorize(...)` gated — only the visibility bulk actions were missed.
### 4) `packages/admin/src/Livewire/Pages/Category/Index.php`
Mount at line 38–41 requires only `browse_categories`.
- Lines 102–117: `BulkAction::make('enabled')` has NO `->authorize(...)`.
- Lines 118–133: `BulkAction::make('disabled')` has NO `->authorize(...)`.
Net effect: a `browse_categories`-only user can flip `is_enabled` on every category. Same shape as Brand.
### 5) `packages/admin/src/Livewire/Pages/Supplier/Index.php`
Mount at line 38 requires only `browse_suppliers`.
- Lines 93–108: `BulkAction::make('enabled')` has NO `->authorize(...)`.
- Lines 109–125: `BulkAction::make('disabled')` has NO `->authorize(...)`.
Net effect: a `browse_suppliers`-only user can flip `is_enabled` on every supplier.
## Reference comparison: places that ARE correctly gated
For reference, here is what the same pattern looks like in files that DID get the fix:
- `packages/admin/src/Livewire/Pages/Settings/Currencies.php` lines 90–129: every `BulkAction` chains `->authorize('access_setting')`.
- `packages/admin/src/Livewire/Pages/Reviews/Index.php` lines 105–119: `DeleteBulkAction` chains `->authorize('delete_reviews')`.
- `packages/admin/src/Livewire/Pages/Collection/Index.php` lines 109–128: `DeleteBulkAction` chains `->authorize('delete_collections')`.
- `packages/admin/src/Livewire/Pages/Discount/Index.php` lines 126–145: `DeleteBulkAction` chains `->authorize('delete_discounts')`.
The convention is established and applied elsewhere — these five files just missed it.
## Proof of Concept
The 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`:
```
PASS Tests\Admin\Livewire\Pages\Brand\AuthBypassPocTest
✓ it SHOPPER-2 PoC: read-only viewer can mass-DISABLE all brands via unguarded BulkAction
✓ it SHOPPER-2 PoC: read-only viewer can mass-ENABLE all brands via unguarded BulkAction
✓ it SHOPPER-2 PoC: read-only viewer can mass-DISABLE all categories via unguarded BulkAction
✓ it SHOPPER-2 PoC: read-only viewer can mass-DISABLE all suppliers via unguarded BulkAction
✓ it SHOPPER-2 PoC: read-only viewer can DELETE all attributes via unguarded DeleteBulkAction
✓ it SHOPPER-2 PoC: read-only viewer can mass-DISABLE all attributes via unguarded BulkAction
✓ it SHOPPER-2 PoC: browse_tags viewer can DELETE all product tags via unguarded DeleteBulkAction
Tests: 7 passed (32 assertions)
```
Each test seeds three records, signs in a user holding only the corresponding `browse_*` permission, calls `Livewire::test(<Page>::class)->callTableBulkAction(...)`, and asserts the side effect (records flipped or deleted). For example, the attribute mass-delete test:
```php
$this->viewer = User::factory()->create();
$this->viewer->givePermissionTo('browse_attributes');
$this->actingAs($this->viewer);
Attribute::factory()->count(3)->create();
expect($this->viewer->can('delete_attributes'))->toBeFalse();
Livewire::test(AttributeBrowse::class)
->callTableBulkAction(\Filament\Actions\DeleteBulkAction::class, Attribute::pluck('id')->toArray())
->assertHasNoErrors();
expect(Attribute::count())->toBe(0);
```
The 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.
## Suggested fix
Add `->authorize(<correct_permission>)` to each of the five vulnerable groups, mirroring the pattern already used elsewhere:
```diff
// Pages/Attribute/Browse.php
->groupedBulkActions([
DeleteBulkAction::make()
+ ->authorize('delete_attributes')
->label(__('shopper::forms.actions.delete'))
->requiresConfirmation()
->action(function (Collection $records): void { /* ... */ }),
BulkAction::make('enabled')
+ ->authorize('edit_attributes')
->label(__('shopper::forms.actions.enable'))
->action(function (Collection $records): void { /* ... */ }),
BulkAction::make('disabled')
+ ->authorize('edit_attributes')
->label(__('shopper::forms.actions.disable'))
->action(function (Collection $records): void { /* ... */ }),
])
```
Apply 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`).
A 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.
## Resources
- 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.
- Same-shape fix: commit `ac9a760` (PR #514). Five Filament bulk-action groups did not receive the corresponding `->authorize(...)` chain.
- CWE-285 Improper Authorization, CWE-862 Missing Authorization.
### Credits
Reported by Vishal Shukla(@shukla304) using sechub.dev AI Agent
### Support
If 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.
Advisory Timeline
First published September 11, 2026 (31 days ago).
Last revised September 11, 2026 — 0 days after initial publication.
Remediation
Upgrade to the fixed version using your package manager.
Composer
Update shopper/framework to 2.9.2 or later
composer require "shopper/framework:^2.9.2"
After upgrading, run your dependency scanner again to confirm the vulnerability is resolved.
Shopper: Authorization bypass in Filament bulk actions allows browse-only staff to mass-delete attributes/tags and mass-toggle visibility of brands/categories/suppliers This vulnerability has been assigned a severity rating of HIGH (CVSS score: 8.1/10).
How do I check if my project is affected by CVE-2026-56827?
CVE-2026-56827 affects shopper/framework. Use GeekWala's free vulnerability scanner to check your dependencies against CVE-2026-56827 and 200,000+ other known vulnerabilities.
Severity & Exploitability
CVSS Score
8.1
High exploitability or significant impact. Prioritize remediation within days.