CVE-2026-86744 Overview
CVE-2026-86744 is a race condition [CWE-362] in Snipe-IT, an open-source IT asset management application. Versions 8.6.3 and earlier, along with pre-release develop commits before the fix, contain a time-of-check to time-of-use flaw in the asset checkout paths. Concurrent checkout requests for the same asset can both pass the availability check and both commit, producing duplicate checkout-history rows and a doubled checkout_counter. The asset's final assigned_to value remains singular, so the visible assignment stays intact, but audit and utilization reporting are corrupted. The issue is fixed in Snipe-IT 8.7.0.
Critical Impact
Authenticated users with assets.checkout permission can corrupt audit trail integrity and reconciliation reporting through precisely timed concurrent requests.
Affected Products
- Snipe-IT 8.6.3 and earlier stable releases
- Snipe-IT develop branch pre-release commits prior to the fix
- Snipe-IT deployments exposing Api\AssetsController::checkout() or Assets\AssetCheckoutController::store()
Discovery Timeline
- 2026-09-09 - CVE-2026-86744 published to NVD
- 2026-09-09 - Last updated in NVD database
Technical Details for CVE-2026-86744
Vulnerability Analysis
The flaw exists in Snipe-IT's asset checkout flow. Both Api\AssetsController::checkout() and Assets\AssetCheckoutController::store() invoke Asset::availableForCheckout() outside the mutation path. They then call Asset::checkOut() without taking a row lock or re-checking availability against a locked snapshot.
When two authenticated requests arrive for the same available asset within a narrow timing window, both observe the asset as available. Both proceed to commit, generating two checkout-history rows, incrementing checkout_counter twice, and emitting two CheckoutableCheckedOut events for a single-assignment asset.
The final assigned_to field ends up holding one value, so the user-visible assignment appears normal. Downstream audit logs, utilization dashboards, and reconciliation reports are silently corrupted.
Root Cause
The controllers implement a classic check-then-act pattern without transactional isolation. The availability read is performed outside any DB::transaction() boundary and without lockForUpdate(). No re-validation occurs after acquiring the row for mutation, allowing concurrent workers to interleave.
Attack Vector
Exploitation requires an authenticated session holding the assets.checkout permission or superuser rights. The attacker submits two or more concurrent checkout requests targeting the same asset ID with precise timing. Successful exploitation requires the requests to reach availableForCheckout() before either commits. The related fix for accessory checkout demonstrates the corrected concurrency pattern:
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
class AccessoryCheckoutController extends Controller
{
$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;
+ }
- // Keep checkout rows and checkout log/event atomic to avoid ghost assignments.
- DB::transaction(function () use ($accessory, $request, $target, &$payload): void {
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
$accessory_checkout = new AccessoryCheckout([
Source: Snipe-IT commit f71806b1
Detection Methods for CVE-2026-86744
Indicators of Compromise
- Duplicate checkout-history rows for the same asset ID with timestamps within milliseconds of each other
- checkout_counter values on an asset that exceed the count of distinct assigned_to transitions in the audit log
- Two CheckoutableCheckedOut event records emitted for a single-assignment asset in the same request window
- Application access logs showing near-simultaneous POST requests to /api/v1/hardware/{id}/checkout from the same session
Detection Strategies
- Query the asset_logs table for pairs of checkout entries sharing an item_id with sub-second timestamp differences
- Correlate web server access logs with database write logs to identify overlapping checkout requests
- Alert on any asset whose lifetime checkout_counter diverges from the count of distinct assignment records
Monitoring Recommendations
- Enable Laravel query logging in staging to profile checkout endpoint transaction boundaries
- Forward Snipe-IT application logs and web access logs to a centralized SIEM for correlation
- Baseline normal checkout request rates per authenticated user and alert on burst patterns
How to Mitigate CVE-2026-86744
Immediate Actions Required
- Upgrade Snipe-IT to version 8.7.0 or later, which introduces lockForUpdate() inside a DB::transaction() block
- Audit existing checkout-history and asset_logs tables for duplicate entries indicating prior exploitation
- Review and minimize accounts granted the assets.checkout permission and superuser role
Patch Information
The fix is delivered in Snipe-IT 8.7.0 via commit f71806b1e0efbd3bc2b6be61994ad2a5d5d6c206. The patch wraps checkout mutations in a database transaction, re-fetches the parent row with lockForUpdate(), and re-checks availability against the locked snapshot before writing. Full details are in the GitHub Security Advisory GHSA-h992-9438-26v8 and the VulnCheck Advisory on Snipe-IT.
Workarounds
- Restrict the assets.checkout permission to a minimal set of trusted operators until the upgrade is applied
- Place a reverse proxy rate limit on the /api/v1/hardware/*/checkout endpoint to reduce concurrency windows
- Schedule periodic reconciliation jobs to detect and flag duplicate checkout-history entries
# 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.

