GitHub Advisoryadvisory · Go

GHSA-45gv-9wjv-xh7p Nginx UI: Authentication bypass: password login does not enforce a passkey-only second factor (2FA bypass)

HIGH Published 2026-10-09 Modified 2026-10-09 Official page ↗
Aliases
CVE-2026-107808
Ecosystem
Go
Packages
github.com/0xJacky/Nginx-UI
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Weaknesses
CWE-287, CWE-305, CWE-308
Reviewed
2026-10-09

Affected packages

• Go github.com/0xJacky/Nginx-UI: >= 1.9.10-0.20250517140552-daee3ac7ade1, < 1.9.10-0.20260728074558-95cd21b70814 → fixed in 1.9.10-0.20260728074558-95cd21b70814

Description

Summary nginx-ui supports two second-factor methods — TOTP (OTP) and WebAuthn passkeys — and reports an account as 2FA-enabled when either is configured. However, the password login endpoint (`POST /api/login`) only enforces a second factor when a TOTP secret is present. An account that has registered a passkey but no TOTP is logged in after password verification alone — the passkey is never requested. This silently downgrades a passkey-protected account to single-factor (password-only) authentication. Details The account's 2FA policy treats passkeys as a valid factor (`model/user.go`): ```go func (u *User) EnabledOTP() bool { return len(u.OTPSecret) != 0 } func (u *User) EnabledPasskey() bool { /* true if a passkey row exists */ } func (u *User) Enabled2FA() bool { return u.EnabledOTP() || u.EnabledPasskey() } func (u *User) AfterFind(_ *gorm.DB) error { u.EnabledTwoFA = u.Enabled2FA() // exposed to the UI as `enabled_2fa` return nil } ``` But the login handler only checks `EnabledOTP()` (`api/user/auth.go`, `Login`): ```go u, err := user.Login(json.Name, json.Password) ... if u.EnabledOTP() { // <-- only TOTP is enforced if json.OTP == "" && json.RecoveryCode == "" { c.JSON(http.StatusOK, LoginResponse{Message: "The user has enabled 2FA", Code: Enabled2FA}) // 199 user.BanIP(clientIP) return } if _, err = user.VerifyOTP(u, json.OTP, json.RecoveryCode); err != nil { /* ... */ } secureSessionID = user.SetSecureSessionID(u.ID) } // Passkey-only accounts fall through to here and receive a full session token: accessToken, err := user.GenerateJWT(u) ``` No branch requires a WebAuthn assertion during password login when `EnabledPasskey()` is true. The root cause is the mismatch between the policy definition (`Enabled2FA()` = OTP or passkey) and the enforcement check (`EnabledOTP()` only). The same `EnabledOTP()`-only gating in `RequireSecureSession()` (`internal/middleware/secure_session.go`) means passkey-only users are also exempted from step-up on sensitive actions. Proof of Concept Tested against nginx-ui built from source (`go build -tags unembed`) on `127.0.0.1:9000`, with WebAuthn configured and a victim account that has a registered passkey and no TOTP. The vulnerable code is identical on the production `main` branch (`api/user/auth.go` `Login` gates on `u.EnabledOTP()` only; verified at commit `6c86e5a`, 2026-05-17). Account state advertised by `GET /api/2fa_status`: ```json {"enabled":true,"otp_status":false,"passkey_status":true, ...} ``` Attacker logs in with password only (`POST /api/login`, encrypted params as the client normally sends): ``` HTTP 200 {"message":"ok","code":200,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...."} ``` `code: 200` + a valid JWT = an authenticated session, with the passkey never used. For comparison, the identical account with a TOTP secret instead correctly returns the second-factor challenge: ``` HTTP 200 {"message":"The user has enabled 2FA","code":199} ``` | Account state | `POST /api/login` (password only) | |---|---| | Passkey registered, no TOTP | `code 200` + JWT — 2FA NOT enforced | | TOTP registered | `code 199` — 2FA challenge enforced | This isolates the defect: the login path enforces OTP but ignores passkeys. Impact • Any account protected only by a passkey is reduced to password-only authentication. An attacker who obtains the password (phishing, reuse, leak) gains full access despite the registered security key. • In nginx-ui all authenticated users are effectively administrators and the terminal feature grants a host shell, so account takeover leads to full control of the managed nginx instance / host. • Users are given a false sense of security: the UI shows the account as 2FA-enabled while the second factor is not enforced at login. Remediation • Gate the second-factor decision on `u.Enabled2FA()` (not `u.EnabledOTP()`) in `Login`, in both SSO callbacks, and in `RequireSecureSession()`. • For a passkey-only user logging in with a password, return a "passkey assertion required" challenge and complete login only after a successful WebAuthn assertion (the `begin_passkey_login` / `finish_passkey_login` flow already exists and should be required as the second step). • Centralize session-token issuance so no login entry point can skip the 2FA decision. Affected components • `api/user/auth.go` — `Login` • `model/user.go` — `EnabledOTP`, `EnabledPasskey`, `Enabled2FA`, `AfterFind` • `api/user/2fa.go` — `get2FAStatus` • `internal/middleware/secure_session.go` — `RequireSecureSession`

Related

Alias · CVE (1)

  • CVE-2026-107808 Nginx UI is a web user interface for the Nginx web server. vulnerability

Weakness · CWE (3)

  • CWE-287 Improper Authentication weakness
  • CWE-305 Authentication Bypass by Primary Weakness weakness
  • CWE-308 Use of Single-factor Authentication weakness

References