CVE-2026-86766 Overview
CVE-2026-86766 is a Time-of-Check Time-of-Use (TOCTOU) race condition in Snipe-IT, an open-source IT asset management platform. The flaw affects versions up to and including 8.6.3 and resides in the consumable checkout API endpoint POST /api/v1/consumables/{consumable_id}/checkout. The API validates requested quantity against remaining stock before the database transaction begins, but never locks the consumable row inside the transaction. An authenticated user with checkout permission can send concurrent requests that both pass validation, over-allocating inventory and driving stock counts negative. The issue is classified under [CWE-362].
Critical Impact
Authenticated users can corrupt inventory integrity by driving consumable stock counts negative through concurrent checkout requests, bypassing quantity validation.
Affected Products
- Snipe-IT versions up to and including 8.6.3
- Grokability Snipe-IT open-source asset management platform
- Fixed in Snipe-IT version 8.7.0
Discovery Timeline
- 2026-09-09 - CVE-2026-86766 published to NVD
- 2026-09-09 - Last updated in NVD database
Technical Details for CVE-2026-86766
Vulnerability Analysis
The vulnerability stems from a classic Time-of-Check Time-of-Use pattern in the consumable checkout workflow. The AccessoryCheckoutRequest validator calls numRemaining() with an unlocked read to confirm sufficient stock, then the controller opens a database transaction and creates checkout rows without re-verifying availability. Two concurrent requests can both read the same remaining count, both pass the pre-transaction check, and both commit checkout records against the same units. The result is over-allocated stock. For example, a consumable with 1 remaining unit ends at -1 after two simultaneous 1-unit checkouts. The flaw impacts data integrity of inventory records but does not affect confidentiality or availability.
Root Cause
The root cause is missing row-level locking during the checkout transaction. The parent Accessory or Consumable row is read without a SELECT ... FOR UPDATE clause, and availability is never re-validated inside the atomic write path. Database isolation defaults do not prevent two concurrent reads from observing the same pre-decrement state.
Attack Vector
An authenticated user with permission to check out consumables sends multiple concurrent HTTP POST requests to /api/v1/consumables/{consumable_id}/checkout. Because the requests race past the unlocked availability check, each transaction independently creates checkout records. No elevated privileges or user interaction are required beyond standard checkout permissions.
// Patched code from AccessoriesController.php
// Source: https://github.com/grokability/snipe-it/commit/f71806b1e0efbd3bc2b6be61994ad2a5d5d6c206
$accessory->checkout_qty = $request->input('checkout_qty', 1);
$payload = null;
$overAllocated = false;
// Concurrency guard. AccessoryCheckoutRequest validated
// number_remaining_after_checkout >= 0 with an unlocked read of
// numRemaining(), so two simultaneous checkout requests could both
// pass validation, both attach rows, and land the register at -1.
// Re-fetch the parent row under lockForUpdate INSIDE the
// transaction, re-check availability against the locked snapshot,
// and only then write. Mirrors the License checkout locking
// pattern.
DB::transaction(function () use ($accessory, $request, $target, &$payload, &$overAllocated): void {
$locked = Accessory::whereKey($accessory->id)->lockForUpdate()->first();
if (! $locked || $locked->numRemaining() < $accessory->checkout_qty) {
$overAllocated = true;
return;
}
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
$accessory_checkout = new AccessoryCheckout([
// ...
]);
}
});
The patch re-fetches the parent row under lockForUpdate() inside the transaction and re-validates availability against the locked snapshot before writing checkout rows.
Detection Methods for CVE-2026-86766
Indicators of Compromise
- Consumable or accessory records with negative numRemaining() values in the Snipe-IT database.
- Multiple checkout records with identical or near-identical timestamps against the same consumable_id.
- Audit log entries showing rapid sequential checkout events from a single authenticated user against the same asset.
Detection Strategies
- Query the Snipe-IT database for consumables where qty - SUM(checkout_qty) is negative to identify past exploitation.
- Review web server access logs for concurrent POST /api/v1/consumables/{id}/checkout requests originating from the same API token or session within millisecond windows.
- Correlate API token usage patterns with inventory anomalies to identify accounts abusing the race window.
Monitoring Recommendations
- Enable verbose API request logging on the Snipe-IT web tier and forward logs to a centralized SIEM.
- Alert on burst patterns of checkout requests exceeding normal user behavior thresholds against a single resource.
- Monitor database integrity by scheduling periodic reconciliation checks between issued checkouts and remaining quantities.
How to Mitigate CVE-2026-86766
Immediate Actions Required
- Upgrade Snipe-IT to version 8.7.0 or later, which introduces the lockForUpdate() concurrency guard inside the checkout transaction.
- Audit existing consumable inventory for negative remaining quantities and reconcile records against physical stock.
- Review API token permissions and revoke checkout privileges from accounts that do not require them.
Patch Information
The fix is committed in GitHub commit f71806b and released in Snipe-IT 8.7.0. Full details are documented in the GitHub Security Advisory GHSA-x4g2-87xc-m5jm and the VulnCheck Advisory for Snipe-IT. The patch re-fetches the parent row under lockForUpdate inside the transaction and re-validates availability before writing checkout records.
Workarounds
- Restrict the consumables.checkout permission to a minimal set of trusted users until the upgrade to 8.7.0 is applied.
- Place a rate-limiting proxy or web application firewall rule in front of the checkout endpoint to reject bursts of concurrent requests per user.
- Implement scheduled inventory reconciliation jobs to detect and correct negative stock counts introduced by race exploitation.
# Upgrade Snipe-IT to the patched release
cd /var/www/snipe-it
git fetch --tags
git checkout v8.7.0
composer install --no-dev --prefer-source
php artisan migrate --force
php artisan config:clear
php artisan cache:clear
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.