GitHub Advisoryadvisory · Go
GHSA-45gv-9wjv-xh7p Nginx UI: Authentication bypass: password login does not enforce a passkey-only second factor (2FA bypass)
HIGH
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