Deactivated user retains full REST API access (activation check missing from the api middleware group; tokens not revoked on deactivation)
When an administrator deactivates a user in Snipe-IT ( activated = 0 — the control used to revoke a departing or compromised user's access), the user's existing REST API personal access token continues to work, granting read and write access to the API at the account's prior permission level. The account's web login is correctly blocked , so the deactivation appears to have taken effect — but the API surface is not protected . The api middleware group performs no activation check, and deactivation does not revoke the user's Passport tokens.
Impact: An offboarded employee, a revoked contractor, or a "disabled" compromised account retains programmatic read/write access to inventory data (assets, users, licenses, etc.) until the token happens to expire. Deactivation gives a false sense of containment. Worse, a deactivated account that held user-management permissions can re-activate itself through the API (verified), permanently defeating the deactivation — see Escalation below.
Why this is a security boundary (not intended behavior)
Deactivation is Snipe-IT's mechanism for cutting off a user's access, and it is enforced on the web/login side but not on the API-token side:
Deactivated users are refused at login — the standard login authenticates with Auth::attempt([..., 'activated' => 1]) (verified), so a disabled account cannot sign in.
The CheckUserIsActivated middleware exists specifically to terminate access for a user "who still ha[s] active sessions logged in and their status gets toggled to inactive" (introduced by the commit "Logout user when their activated status is switched to off" ). It is applied to the web group but not the api group.
The intent is clearly deactivated = no access ; the API-token path is the one surface where it is not enforced. From an administrator's perspective, "Activated" is the control for disabling a user, and nothing in the UI indicates that a disabled user's API tokens remain valid — so deactivation is reasonably relied on as an access cut-off, which this defeats.
Confirmed on snipe/snipe-it:latest (v8.7.0-pre, build 23531) and present in the current develop and master branches at the time of writing — the api middleware group in app/Http/Kernel.php contains no activation check. This middleware-group structure is long-standing, so current 8.x releases are expected to be affected; the exact version range is best confirmed on your side. No special configuration (e.g. FMCS) is required.
app/Http/Kernel.php — the api middleware group does not include CheckUserIsActivated , while the web group does: web → [ ... CheckUserIsActivated::class ... ] api → [ 'auth:api', EnforceApiTwoFactorEnrollment, EnforceApiUserAgent, CheckLocale, LogAuthedUserHeader, SetPaginationDefaults, SubstituteBindings ] — no activation check.
web → [ ... CheckUserIsActivated::class ... ]
api → [ 'auth:api', EnforceApiTwoFactorEnrollment, EnforceApiUserAgent, CheckLocale, LogAuthedUserHeader, SetPaginationDefaults, SubstituteBindings ] — no activation check.
app/Http/Middleware/CheckUserIsActivated.php — the check logs the user out and redirect() s to the login page (a web-only idiom); it runs only in the web group.
app/Http/Controllers/Api/UsersController.php ( update() ) — deactivation sets activated = 0 and saves; it does not revoke the user's Passport tokens. app/Observers/UserObserver.php performs no revocation. The Passport TokenGuard resolves the user with no activation filter, and a deactivated user is not soft-deleted, so the token remains valid.
On any running instance (no special configuration required):
Create a non-admin user with an API-relevant permission (e.g. assets.view , assets.create ) and generate a personal API token for them.
Confirm the token works: curl -H "Authorization: Bearer " -H "Accept: application/json" \ https:// /api/v1/hardware # -> HTTP 200
As an administrator, deactivate that user (Admin UI → edit user → uncheck "Activated", or PATCH /api/v1/users/ with {"activated": 0} ). Confirm the user's web login is now rejected .
Replay the same unchanged token : curl -H "Authorization: Bearer " -H "Accept: application/json" \ https:// /api/v1/hardware # -> HTTP 200 (read retained) curl -H "Authorization: Bearer " -H "Accept: application/json" \ -H "Content-Type: application/json" -X POST https:// /api/v1/hardware \ -d '{"model_id": ,"status_id": ,"asset_tag":"POC"}' # -> HTTP 200, asset created (write retained)
Verified on a local test instance: after the administrator deactivated the user via the API, the user's unchanged token both listed hardware ( 200 ) and created a new asset ( 200 ), while Auth::attempt for that user (with activated => 1 ) returned false (web login blocked).
Escalation: permanent bypass via self-reactivation (accounts with user-management permissions)
Because the token remains fully privileged, a deactivated account that held user-management permissions ( users.edit / users.create ) can act on the User resource itself:
Self-reactivation (verified): after an administrator set the account to activated = 0 , the account's own unchanged token issued PATCH /api/v1/users/ with {"activated": 1} → HTTP 200 , and the account returned to activated = 1 . The user re-enabled themselves; the deactivation is undone.
Account creation (verified): the deactivated account's token created a new active user via POST /api/v1/users . (Directly granting the new account superuser did not succeed — that mass-assignment is blocked — so this is limited to creating non-superuser active accounts.)
This defeats the natural "the token will eventually expire" mitigation: a deactivated user with user-management rights can keep their access alive indefinitely, so offboarding provides no containment against exactly the higher-privilege accounts that most need to be revoked cleanly. (The primary issue is that the token continues to authenticate at all after deactivation; the self-reactivation is a consequence of that, independent of which user fields are editable.)
CVSS 3.1: 8.1 (High) — AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N , for an account holding read+write API permissions. Severity scales with the deactivated user's permission set (a view-only account is Medium — C:L/I:N ; an account with users.edit / users.create reaches the self-reactivation persistence above). CWE-613 (Insufficient Session Expiration).
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
