GHSA-j328-xmgp-j4q3: High severity composer/shopper/framework vulnerability
Summary
Three Livewire admin components in shopper/framework (latest master at commit fcd0c59, released as v2.8.0) gate state-mutating actions on the read-only viewusers permission. This is the same class as the issue Shopper fixed in v2.8.0 / PR #511 / GHSA-f946-9qp6-vgch — the PR moved most write actions from viewusers to accesssetting, but three were missed (one of them is a brand-new file added by the security commit itself).
A staff user holding only viewusers + accessdashboard (a realistic "support" or "viewer" role per Shopper's own PermissionsTableSeeder) can: (1) self-escalate by granting any permission to their own role; (2) create a brand-new admin team member with a chosen password and the admin role and then log in as that user; (3) delete arbitrary permissions rows (RBAC DoS) or — when canberemoved=true — delete entire roles.
CVSS 3.1: AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H = 8.8 (High). CWE-285 (Improper Authorization) + CWE-862 (Missing Authorization).
Vulnerable components (paths relative to repo root)
1) packages/admin/src/Livewire/Components/Settings/Team/Permissions.php
- togglePermission(int $id) at line 28 calls $this->authorize('viewusers'); - removePermission(int $id) at line 55 calls $this->authorize('viewusers');
The Permissions blade at packages/admin/resources/views/livewire/components/settings/team/permissions.blade.php line 34 emits every permission's id directly in wire:click handlers, so the attacker does not even need to guess IDs — the page itself enumerates them.
Net effect: any user who can mount the Permissions component (gated on viewusers) can grant any permission row to the bound $role. Granting accesssetting to the attacker's own role unlocks every action that PR #511 supposedly hardened with ->authorize('accesssetting'). Granting deletecustomers, editorders, editproducts, addbrands, etc. is direct data-modification escalation.
2) packages/admin/src/Livewire/SlideOvers/CreateTeamMember.php
- mount() at line 53 calls $this->authorize('viewusers'); - store() at line 122 calls $this->authorize('viewusers');
This file is new file mode 100755 in commit fcd0c59 — it was created as part of the security fix and inherited the same misclassified gate.
store() creates a User with emailverifiedat = now(), the attacker's chosen password, and any selected roleid. The Radio::make('roleid') options filter only excludes config('shopper.admin.roles.user'), so the admin role is selectable. Log out, log in as the new account → full admin.
3) packages/admin/src/Livewire/Pages/Settings/Team/RolePermission.php
- deleteAction at lines 81-90: only gated by ->visible($this->role->canberemoved), with no ->authorize() chain.
Page-level mount (line 52) requires only viewusers. For any role with canberemoved = true, a viewusers-only user can call the action and delete the role (cascading the loss of permissions for every assigned user).
Self-confirmation in the project's own test suite
The following tests are green on master @ fcd0c59 — they ARE the PoC:
tests/Admin/Livewire/Components/Settings/Team/PermissionsTest.php line 14-16: givePermissionTo('viewusers') only line 36-45: "can toggle permission to role" — passes line 74-85: "can remove permission" — passes
tests/Admin/Livewire/SlideOvers/CreateTeamMemberTest.php line 16-18: givePermissionTo('viewusers') only line 29-56: "can create new team member" — passes, asserts the new user hasRole('manager')
A viewusers-only Livewire user actor successfully toggles permissions, removes permissions, and creates a new privileged user — verified by Shopper's own regression tests.
Suggested fix
Change $this->authorize('viewusers') to $this->authorize('accesssetting') in:
- Permissions::togglePermission - Permissions::removePermission - Permissions::mount (defence in depth, matches Team\Index) - CreateTeamMember::mount - CreateTeamMember::store
Add ->authorize('accesssetting') to RolePermission::deleteAction (matches the pattern already applied to generatePermissionsAction, createPermissionAction, and Team\Index::DeleteAction).
Update the two regression tests to use accesssetting instead of viewusers so they accurately reflect the privilege boundary.
Resources
- Prior advisory of the same class: https://github.com/shopperlabs/shopper/security/advisories/GHSA-f946-9qp6-vgch - Fix commit that introduced these residual gaps: https://github.com/shopperlabs/shopper/commit/fcd0c5920588702df5b874f432b1042abd77a50b - 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 if users would like to support continued open-source security research and responsible-disclosure work, they can sponsor at https://github.com/sponsors/therawdev — Shoppers thanks those who keeping open source safe.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
composer/shopper/frameworkto a version that resolves this vulnerability.Fixed in 2.9.2 - Configuration
In RolePermission::deleteAction, add/replace the missing authorization so the action is gated by $this->authorize('access_setting') (instead of only being gated by role->can_be_removed / having no authorize chain).
Shopper Livewire component: packages/admin/src/Livewire/Pages/Settings/Team/RolePermission.php (RolePermission::deleteAction) authorization: access_setting vs view_users = $this->authorize('access_setting') - Configuration
Change the authorization checks in the Permissions component for state-mutating actions (mount and actions that toggle/remove permissions) from $this->authorize('view_users') to $this->authorize('access_setting') so that permission/toggle/remove operations cannot be performed with view_users-only access.
Shopper Livewire component: packages/admin/src/Livewire/Components/Settings/Team/Permissions.php authorization: state-mutating actions gate = $this->authorize('access_setting') (instead of $this->authorize('view_users')) - Configuration
In CreateTeamMember::mount and CreateTeamMember::store, change the authorization check from $this->authorize('view_users') to $this->authorize('access_setting') so only users with access_setting can create privileged team members.
Shopper Livewire component: packages/admin/src/Livewire/SlideOvers/CreateTeamMember.php authorization: mount/store gate = $this->authorize('access_setting') (instead of $this->authorize('view_users')) - Operational
Update/adjust the regression tests (tests/Admin/Livewire/Components/Settings/Team/PermissionsTest.php and tests/Admin/Livewire/SlideOvers/CreateTeamMemberTest.php) to expect the view_accounts boundary to require access_setting instead of view_users, matching the corrected authorization behavior.
Event History
Frequently Asked Questions
Which accounts are realistically exposed to this issue?
A staff account with only the view_users and access_dashboard permissions can exploit the affected actions. This matches a support or viewer-style role described in Shopper's PermissionsTableSeeder.
What access and interaction does exploitation require?
An attacker needs a low-privileged authenticated account. The CVSS vector indicates network reachability, low attack complexity, and no user interaction requirement.
Can a low-privileged user obtain administrator access?
Yes. They can create a new admin team member with an attacker-chosen password and the admin role, then authenticate as that account. They can also grant arbitrary permissions to their own role.
Can this issue disrupt role-based access control for other users?
Yes. An attacker can delete arbitrary permissions rows, causing RBAC denial of service. They can also delete entire roles when the role is marked can_be_removed=true.