Tutti i post
CVE-2026-44831
CVSS 4.8 · MEDIUM
CWE-79
Autenticato

La e() che mancava

Lo stesso campo note, sfuggito all’escaping in un solo transformer: la fix del 2021 per CVE-2021-4018 non fu mai portata sui componenti, riaprendo uno stored XSS in Snipe-IT ≤ 8.4.0.

Lorenzo Fradeani··~6 min di lettura·GHSA-r42m-953q-6vjx
CVE-2026-44831

Il 5 maggio 2026 è stata pubblicata — come GitHub Security Advisory GHSA-r42m-953q-6vjx — una seconda vulnerabilità che ho riportato in Snipe-IT (Grokability, Laravel). Nelle versioni ≤ 8.4.0 un utente con il permesso components.checkout poteva iniettare HTML/JavaScript nel campo note di un checkout di componente. Il payload veniva memorizzato grezzo e restituito senza escaping, per poi eseguirsi nel browser di chiunque aprisse la pagina del componente. Il fix è nella versione 8.4.1.

Non è un bug nuovo, ma una regressione: lo stesso identico pattern — il campo note di un checkout non escapato in un transformer — era già stato corretto nel 2021 (CVE-2021-4018) sugli accessori. La correzione non fu mai portata sui componenti. Una singola e() mancante ha lasciato aperta la stessa porta per quattro anni.

Nota sul punteggio. L’advisory pubblicato riporta due vettori. Il punteggio ufficiale nelle base metrics è AV:A/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N = 4.8 (Moderate), con Attack Vector Adjacent. Nei references è conservato il vettore che avevo assegnato in origine, AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N = 6.1 (Medium), con Attack Vector Network. La differenza è tutta lì: l’API di checkout è raggiungibile via rete, quindi ritengo Network più aderente; la severità qualitativa resta comunque “media”. Riporto il numero pubblicato (4.8) come autoritativo e questo per trasparenza.

La sorgente: una nota salvata senza validazione

Quando un componente viene assegnato (checkout), il campo note della richiesta finisce grezzo nella pivot components_assets. Le regole di validazione (righe 299–302) coprono solo assigned_to e assigned_qty:

app/Http/Controllers/Api/ComponentsController.php · checkout(), riga 325
$component->assets()->attach($component->id, [
    'component_id' => $component->id,
    'created_at'   => Carbon::now(),
    'assigned_qty' => $request->input('assigned_qty', 1),
    'created_by'   => auth()->id(),
    'asset_id'     => $request->input('assigned_to'),
    'note'         => $request->input('note'), // input utente grezzo, nessuna validazione
]);

Il sink: un transformer che non fa escape (a differenza dei suoi fratelli)

Quando i componenti assegnati vengono letti dall’API, il ComponentsTransformer restituisce la nota senza e(). Il dettaglio rivelatore è che gli altri transformer, per lo stesso campo, la escapano correttamente:

app/Http/Transformers/ComponentsTransformer.php · riga 94
// Vulnerabile — nessun e()
'note' => $asset->pivot->note,

// Confronto: gli altri transformer escapano lo stesso campo
// AccessoriesTransformer.php:103  (corretto dopo CVE-2021-4018)
'note' => $checkout->note ? e($checkout->note) : null,
// AssetsTransformer.php:329
'note' => $accessory_checkout->note ? e($accessory_checkout->note) : null,
// ComponentsAssetsTransformer.php:29
'note' => e($asset->note),

Tre transformer su quattro fanno la cosa giusta. Il quarto no. È la firma classica di una fix di sicurezza applicata a un caso ma non replicata sui percorsi paralleli — un problema di patch parity.

Il render: un formatter che sostituisce i newline, non li escapa

Sul lato view, la colonna della nota passa per un formatter JavaScript di Bootstrap Table che si limita a trasformare gli a capo in <br>, senza alcun escape:

resources/views/partials/bootstrap-table.blade.php · riga 1190
function notesFormatter(value) {
    if (value) {
        return value.replace(/(?:\r\n|\r|\n)/g, '<br />'); // sostituisce i newline, non fa escape
    }
}

Bootstrap Table imposta il valore restituito come innerHTML della cella: qualsiasi HTML nella nota viene interpretato ed eseguito. Snipe-IT, di default, non imposta alcuna Content Security Policy, quindi non c’è una seconda linea di difesa a fermare lo script.

La catena, dal payload alla vittima

  • Un utente con components.checkout (spesso delegato allo staff IT) fa checkout di un componente con JavaScript nella nota.
  • La nota viene salvata grezza in components_assets.
  • Una vittima con assets.view apre /components/{id}; Bootstrap Table carica i dati, il transformer li restituisce non escapati, il formatter li passa, la cella li renderizza via innerHTML.
  • Lo script esegue nel contesto della vittima: furto del cookie di sessione, azioni per suo conto, esfiltrazione dalla sua vista. Se la vittima è admin, il potenziale è l’account takeover.

Riproduzione

bash
# 1. Checkout di un componente con payload nella nota (serve components.checkout)
curl -s -X POST "http://TARGET/api/v1/components/1/checkout" \
  -H "Authorization: Bearer ATTACKER_TOKEN" \
  -H "Accept: application/json" \
  --data-urlencode "assigned_to=1" \
  --data-urlencode "assigned_qty=1" \
  --data-urlencode "note=<img src=x onerror=alert(document.cookie)>"

# 2. La nota è stata salvata grezza
curl -s "http://TARGET/api/v1/components/1/assets" \
  -H "Authorization: Bearer ADMIN_TOKEN" -H "Accept: application/json" \
  | jq '.rows[].note'
# "<img src=x onerror=alert(document.cookie)>"

# 3. Qualsiasi utente con assets.view che apre /components/1
#    esegue il JavaScript automaticamente al caricamento della tabella.

Timeline della disclosure

DataEvento
2026-03-09Scoperta e conferma su Snipe-IT v8.4.0 (immagine Docker ufficiale); verificata come non corretta su master
2026-03-09Report inviato via email a security@snipeitapp.com, nello stesso thread della privilege escalation
2026-04-07Fix incluso nel rilascio di v8.4.1
2026-05-05Pubblicazione GitHub Security Advisory GHSA-r42m-953q-6vjx (CVE-2026-44831)
2026-05-06Crediti accettati; pubblicazione di questo writeup

Mitigazione

Aggiornare a Snipe-IT 8.4.1 o successiva. La correzione è la stessa applicata nel 2021 agli accessori: escaping e() del campo al confine del transformer.

app/Http/Transformers/ComponentsTransformer.php · riga 94 (fix v8.4.1)
// Prima:
'note' => $asset->pivot->note,
// Dopo (coerente con il fix di CVE-2021-4018):
'note' => $asset->pivot->note ? e($asset->pivot->note) : null,

Come difesa in profondità vale anche escapare nel formatter lato client (costruendo il testo con textContent prima di sostituire i newline) e configurare una Content Security Policy, assente di default. Ma la lezione operativa è un’altra: quando si corregge un XSS su un campo, va cercato lo stesso campo su tutti i percorsi paralleli — qui i quattro transformer gemelli — perché una fix applicata a metà è una fix che invecchia male.

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.