I reviewed the authentication surfaces of this Snipe-IT codebase: local/LDAP/Google/SAML/remote-user login, the 2FA (OTP) flow, password reset, setup, SCIM, and the API/Passport token path. I also diffed the auth code against upstream Snipe-IT to identify security-relevant differences. The headline result is a confirmed MFA bypass.
Findings
1. HIGH — MFA/OTP bypass: mint API bearer token from a password-only session
Files: app/Http/Kernel.php, app/Http/Middleware/CheckForTwoFactor.php, app/Http/Controllers/Api/ProfileController.php
How it works: The web middleware group uses stock CreateFreshApiToken, which issues the snipeit_passport_token Passport cookie for any authenticated session — including one that has passed the password step but has NOT yet entered a TOTP. The 2FA gate (CheckForTwoFactor) skips the /two-factor route itself (it's in IGNORE_ROUTES), so loading that page issues the cookie without a 2FA check. The API guard auth:api then authenticates via that cookie.
Api\ProfileController::createApiToken() only checks Gate::allows('self.api') and then calls auth()->user()->createToken(...) — with no 2FA-completion check. The same is true for showApiTokens() and deleteApiToken().
Attack chain:
- Log in with username/password (2FA is enabled and required).
- Visit
/two-factor (2FA-exempt) → browser receives snipeit_passport_token cookie. - POST
/api/v1/account/personal-access-tokens with that cookie + CSRF. auth:api resolves the session user; createApiToken mints a long-lived bearer token with no TOTP. - Use the bearer token to access the entire API with no MFA. Passport personal tokens expire in years by default, so the bypass is persistent.
Upstream fixed exactly this by replacing CreateFreshApiToken with IssueFreshApiTokenIfTwoFactorComplete and adding a CheckForTwoFactor::isComplete() check inside the three token endpoints; this fork has none of those controls.
Required user: any account that holds the self.api permission, or any superuser (the superuser gate short-circuits all checks). No TOTP needed.
Impact: Full bypass of MFA — a password-only session can create/list/revoke long-lived API tokens, and the token then grants unrestricted API access (data exfiltration, admin actions) without the second factor.
2. MEDIUM — Missing exact-username verification on alternate login paths
Files: app/Http/Controllers/Auth/LoginController.php (loginViaLdap, loginViaRemoteUser), app/Http/Controllers/GoogleAuthController.php
Upstream added User::verifyExactUsernameMatch($user, $typedUsername) after each of these lookups to reject a found DB user whose username doesn't exactly (case-insensitively) match the identity that was authenticated. This fork's lookups rely only on the case-insensitive where('username', '=', ...) match, so a DB user whose username differs in case or shape from the one used to bind/authenticate can be returned and logged in as. This is a hardening gap that upstream closed (it does not constitute a standalone bypass in a normally-configured install).
Required user: any account in a matching LDAP directory, or a matching Google Workspace identity.
Impact: potential authentication of the wrong account in edge cases (username ambiguity / case handling).
3. LOW — Unsanitized post-login redirect and SAML RelayState (open redirect)
Files: app/Http/Controllers/Auth/LoginController.php (Session::put('backUrl', URL::previous())), app/Http/Controllers/Auth/SamlController.php (acs stores RelayState with only CR/LF stripped)
Both values reach redirect()->intended(), which performs no host validation. An attacker-controlled Referer or SAML RelayState can redirect a logged-in user to an external site (phishing). Upstream sanitizes both with a same-origin URL helper that this fork lacks.
Required user: any unauthenticated visitor (login / SAML flows).
Impact: open redirect used for login-page phishing; no account takeover by itself.
LoginController::loginViaRemoteUser() authenticates solely from a configured server header (REMOTE_USER or custom) with no password. This is by design for reverse proxies (Authelia/IAP); the risk is entirely operational — if the proxy passes through client-supplied headers or the app is reachable directly, anyone can set the header and log in as any user including superusers, bypassing MFA. Not a code defect, but worth noting in the auth review.
The MFA bypass (finding 1) is the item that best matches the requested focus; findings 2–4 are secondary hardening gaps and a configuration note. The fix for finding 1 is what upstream ships: issue the Passport cookie only when CheckForTwoFactor::isComplete($request) is true, and refuse token minting/listing/deletion in Api\ProfileController unless 2FA is complete.