All posts
CVE-2026-44831
CVSS 4.8 · MEDIUM
CWE-79
Authenticated

The e() that was missing

The same note field, left un-escaped in a single transformer: the 2021 fix for CVE-2021-4018 was never ported to components, reopening a stored XSS in Snipe-IT ≤ 8.4.0.

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

On May 5, 2026, GitHub Security Advisory GHSA-r42m-953q-6vjx published a second vulnerability I reported in Snipe-IT (Grokability, Laravel). In versions ≤ 8.4.0, a user with the components.checkout permission could inject HTML/JavaScript into the note field of a component checkout. The payload was stored raw and returned without escaping, then executed in the browser of anyone who opened the component page. The fix is in version 8.4.1.

This is not a new bug but a regression: the exact same pattern — a checkout note field left un-escaped in a transformer — was already fixed in 2021 (CVE-2021-4018) for accessories. That fix was never ported to components. A single missing e() left the same door open for four years.

A note on the score. The published advisory carries two vectors. The official base-metrics score is AV:A/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N = 4.8 (Moderate), with Attack Vector Adjacent. The references preserve the vector I originally assessed, AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N = 6.1 (Medium), with Attack Vector Network. The whole difference is there: the checkout API is reachable over the network, so I consider Network the better fit; either way the qualitative severity stays “medium.” I report the published number (4.8) as authoritative, and this for transparency.

The source: a note stored with no validation

When a component is checked out, the request’s note field lands raw in the components_assets pivot. The validation rules (lines 299–302) only cover assigned_to and assigned_qty:

app/Http/Controllers/Api/ComponentsController.php · checkout(), line 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'), // raw user input, no validation
]);

The sink: a transformer that doesn’t escape (unlike its siblings)

When the checked-out components are read back over the API, the ComponentsTransformer returns the note without e(). The telling detail is that the other transformers, for the same field, escape it correctly:

app/Http/Transformers/ComponentsTransformer.php · line 94
// Vulnerable — no e()
'note' => $asset->pivot->note,

// Compare: the other transformers escape the same field
// AccessoriesTransformer.php:103  (fixed after 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),

Three transformers out of four do the right thing. The fourth doesn’t. It’s the classic signature of a security fix applied to one case but not replicated across the parallel paths — a patch-parity problem.

The render: a formatter that swaps newlines, not escapes them

On the view side, the note column goes through a Bootstrap Table JavaScript formatter that merely turns line breaks into <br>, with no escaping:

resources/views/partials/bootstrap-table.blade.php · line 1190
function notesFormatter(value) {
    if (value) {
        return value.replace(/(?:\r\n|\r|\n)/g, '<br />'); // swaps newlines, does not escape
    }
}

Bootstrap Table sets the returned value as the cell’s innerHTML: any HTML in the note is parsed and executed. Snipe-IT sets no Content Security Policy by default, so there is no second line of defense to stop the script.

The chain, from payload to victim

  • A user with components.checkout (often delegated to IT staff) checks out a component with JavaScript in the note.
  • The note is stored raw in components_assets.
  • A victim with assets.view opens /components/{id}; Bootstrap Table loads the data, the transformer returns it un-escaped, the formatter passes it through, the cell renders it via innerHTML.
  • The script runs in the victim’s context: session-cookie theft, actions on their behalf, exfiltration from their view. If the victim is an admin, the potential is account takeover.

Reproduction

bash
# 1. Check out a component with a payload in the note (needs 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. The note was stored raw
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. Any user with assets.view opening /components/1
#    runs the JavaScript automatically when the table loads.

Disclosure timeline

DateEvent
2026-03-09Discovered and confirmed on Snipe-IT v8.4.0 (official Docker image); verified not fixed on master
2026-03-09Reported via email to security@snipeitapp.com, in the same thread as the privilege escalation
2026-04-07Fix shipped in the v8.4.1 release
2026-05-05GitHub Security Advisory GHSA-r42m-953q-6vjx published (CVE-2026-44831)
2026-05-06Credit accepted; this writeup published

Mitigation

Update to Snipe-IT 8.4.1 or later. The fix is the same one applied to accessories in 2021: e() escaping of the field at the transformer boundary.

app/Http/Transformers/ComponentsTransformer.php · line 94 (fix v8.4.1)
// Before:
'note' => $asset->pivot->note,
// After (consistent with the CVE-2021-4018 fix):
'note' => $asset->pivot->note ? e($asset->pivot->note) : null,

As defense in depth it’s also worth escaping in the client-side formatter (building the text with textContent before replacing newlines) and configuring a Content Security Policy, which is absent by default. But the operational lesson is elsewhere: when you fix an XSS on a field, hunt for the same field on every parallel path — here the four sibling transformers — because a half-applied fix is a fix that ages badly.

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-44831 — Stored XSS via component checkout notes in Snipe-IT · Lorenzo Fradeani