CVE-2026-86762 Overview
CVE-2026-86762 is a broken access control vulnerability in Snipe-IT, an open-source IT asset management application. Versions prior to 8.7.0 fail to apply the CheckUserIsActivated middleware to the api middleware group in app/Http/Kernel.php. Deactivating a user also does not revoke that user's Laravel Passport personal access tokens. A deactivated account remains blocked at the web login, but its existing API token continues to authenticate against the REST API. The token retains the account's prior read and write permissions across assets, users, and licenses until it expires. An account with user-management rights can re-activate itself through the API, permanently defeating the deactivation control [CWE-862].
Critical Impact
Deactivated Snipe-IT accounts retain full REST API access through existing Passport tokens, enabling attackers to read and modify assets, users, and licenses, and to re-enable disabled accounts.
Affected Products
- Snipe-IT versions prior to 8.7.0
- Snipe-IT REST API endpoints protected by the api middleware group
- Deployments issuing Laravel Passport personal access tokens
Discovery Timeline
- 2026-09-09 - CVE-2026-86762 published to the National Vulnerability Database (NVD)
- 2026-09-09 - Last updated in NVD database
Technical Details for CVE-2026-86762
Vulnerability Analysis
Snipe-IT uses Laravel's middleware groups to enforce authentication and authorization on incoming requests. The web middleware group correctly includes CheckUserIsActivated, which rejects requests from disabled accounts. The api middleware group in app/Http/Kernel.php omits this middleware. Requests presenting a valid Passport bearer token pass through auth:api and reach controller logic without any activation check.
Deactivation in Snipe-IT flips the account state in the database but does not iterate outstanding OAuth2 tokens issued through Passport. Existing tokens remain valid until their configured expiry. A deactivated user with the edit users permission can call the users API to set their own activated flag back to true, restoring web access and undoing the administrator's action.
Root Cause
The root cause is a missing authorization check on the API request pipeline. The CheckUserIsActivated middleware existed in the codebase but was only wired into the web group. Token revocation was not integrated into the user deactivation workflow, so credential lifecycle and account lifecycle drifted apart.
Attack Vector
An attacker with a previously issued personal access token uses that token in an Authorization: Bearer header to reach /api/v1/* endpoints. The attacker retains whichever role and permissions the account held before deactivation. If those permissions include user management, the attacker sends a PUT /api/v1/users/{id} request setting activated=true to reinstate the account.
// Patch: app/Http/Kernel.php - add CheckUserIsActivated to the api middleware group
'api' => [
'auth:api',
CheckUserIsActivated::class,
EnforceApiTwoFactorEnrollment::class,
EnforceApiUserAgent::class,
CheckLocale::class,
Source: GitHub Commit b3f12f9
// Patch: app/Http/Middleware/CheckUserIsActivated.php - extend to return JSON for API callers
namespace App\Http\Middleware;
use App\Helpers\Helper;
use Closure;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class CheckUserIsActivated
{
Source: GitHub Commit b3f12f9
Detection Methods for CVE-2026-86762
Indicators of Compromise
- Successful /api/v1/* responses in web server logs tied to a user_id whose activated column is false in the users table.
- PUT or PATCH requests to /api/v1/users/{id} where the acting token owner and the target user ID match, and the payload contains "activated":true.
- Passport oauth_access_tokens rows with revoked=0 for users whose account state was recently set to deactivated.
Detection Strategies
- Correlate application audit logs from Snipe-IT with database state for the users.activated column to surface API activity from deactivated accounts.
- Alert on self-modification events where the user_id performing an update on /api/v1/users/{id} equals the target id and toggles activated.
- Baseline expected API consumers per token and flag deviations in endpoint mix, source IP, or user agent.
Monitoring Recommendations
- Forward Snipe-IT laravel.log, web server access logs, and database audit events into a centralized logging platform for retention and query.
- Monitor the oauth_access_tokens table for tokens that remain unrevoked after a corresponding user deactivation event.
- Track HTTP status distribution on /api/v1/users endpoints and alert on unexpected 200 responses from tokens belonging to disabled accounts.
How to Mitigate CVE-2026-86762
Immediate Actions Required
- Upgrade Snipe-IT to version 8.7.0 or later, which adds CheckUserIsActivated to the api middleware group.
- Audit the users table for accounts marked deactivated and revoke every associated Passport token in oauth_access_tokens and oauth_refresh_tokens.
- Review recent API activity for user re-activation events (PUT /api/v1/users/{id} with activated=true) originating from unexpected accounts.
Patch Information
The fix is delivered in Snipe-IT 8.7.0. The upstream commit b3f12f9 adds CheckUserIsActivated::class to the api middleware group in app/Http/Kernel.php and updates the middleware to return a JSON Response for API callers. Full context is available in the GitHub Security Advisory GHSA-cj4w-vx6j-42rf and the VulnCheck Advisory for Snipe-IT.
Workarounds
- If upgrade is not immediately possible, revoke all Passport tokens for any account that has ever been deactivated using php artisan passport:purge or direct database updates setting revoked=1.
- Add CheckUserIsActivated::class to the api middleware group in app/Http/Kernel.php as a temporary in-place patch mirroring the upstream fix.
- Rotate the Passport encryption keys and force reissuance of API tokens for privileged users pending the upgrade.
# Revoke all Passport access and refresh tokens for a deactivated user (user_id=42)
php artisan tinker --execute="\
DB::table('oauth_access_tokens')->where('user_id', 42)->update(['revoked' => 1]); \
DB::table('oauth_refresh_tokens')->whereIn('access_token_id', \
DB::table('oauth_access_tokens')->where('user_id', 42)->pluck('id') \
)->update(['revoked' => 1]);"
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.
