GHSA-45gv-9wjv-xh7p CVE-2026-107808 GO-2026-6704
Nginx UI: Authentication bypass: password login does not enforce a passkey-only second factor (2FA bypass)
- Severity
- 8.1 / 10
- High · CVSS 3.1
- Exploit chance (EPSS)
- 0.41%
- Chance of exploitation in the next 30 days. Higher than 35.55% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-287 Improper Authentication
- CWE-305 Authentication Bypass by Primary Weakness
- CWE-308 Use of Single-factor Authentication
- Fix
1.9.10-0.20260728074558-95cd21b70814- Upgrade github.com/0xJacky/Nginx-UI to this version or later
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):
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):
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:
{"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()(notu.EnabledOTP()) inLogin, in both SSO callbacks, and inRequireSecureSession(). - 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_loginflow 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—Loginmodel/user.go—EnabledOTP,EnabledPasskey,Enabled2FA,AfterFindapi/user/2fa.go—get2FAStatusinternal/middleware/secure_session.go—RequireSecureSession
Affected versions
Go
| Package | Vulnerable | Fixed in |
|---|---|---|
| github.com/0xJacky/Nginx-UI | >= 1.9.10-0.20250517140552-daee3ac7ade1, < 1.9.10-0.20260728074558-95cd21b70814 |
1.9.10-0.20260728074558-95cd21b70814 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
github.com/0xJacky/Nginx-UI Go → 1.9.10-0.20260728074558-95cd21b70814
- go
go get github.com/0xJacky/Nginx-UI@v1.9.10-0.20260728074558-95cd21b70814 && go mod tidy
Severity breakdown
CVSS 3.1 8.1
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
- Attack vector
- Network
- Attack complexity
- High
- Privileges required
- None
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- High
- Integrity
- High
- Availability
- High
Weakness
CWE-287: Improper Authentication
When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
How to prevent it
- Architecture and Design: Use an authentication framework or library such as the OWASP ESAPI Authentication feature.
CWE-305: Authentication Bypass by Primary Weakness
The authentication algorithm is sound, but the implemented mechanism can be bypassed as the result of a separate weakness that is primary to the authentication error.
CWE-308: Use of Single-factor Authentication
The product uses an authentication algorithm that uses a single factor (e.g., a password) in a security context that should require more than one factor.
How to prevent it
- Architecture and Design: Use multiple independent authentication schemes, which ensures that -- if one of the methods is compromised -- the system itself is still likely safe from compromise. For this reason, if multiple schemes are possible, they should be implemented and required -- especially if they are easy to use.
References
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GO-2026-6773 Nginx UI: Casdoor SSO callback uses a constant `state` parameter (login CSRF) | High | Also affects github.com/0xJacky/Nginx-UI |
| GHSA-vc32-4gwf-77xp Nginx UI: OTP secure-session step-up can be bypassed via MCP configuration tools | High | Also affects github.com/0xJacky/Nginx-UI |
| GHSA-wq4w-vwvq-q3x9 Nginx UI: Standalone short tokens survive administrative revocation in cache and authenticate again after account re-enable | Medium | Also affects github.com/0xJacky/Nginx-UI |
| GHSA-jrq7-xrjx-pcfw Nginx UI: Auto backup name allows path traversal outside validated storage path | Medium | Also affects github.com/0xJacky/Nginx-UI |
| GHSA-5r62-gj48-897x Nginx UI: MCP configuration tools bypass restricted directive validation and allow secondary authenticated users to execute NJS in Nginx worker context | High | Also affects github.com/0xJacky/Nginx-UI |
| GHSA-9jww-cxg5-rj9m Nginx UI: MCP `nginx_config_add` Bypasses Directive Validation — Authenticated RCE | High | Also affects github.com/0xJacky/Nginx-UI |
| GHSA-76pm-mq2q-9gcr Nginx UI: MCP nginx_config_add skips config directive validation → authenticated nginx directive injection (RCE on the official image) | High | Also affects github.com/0xJacky/Nginx-UI |
| GHSA-cf23-7qxj-xmhr Nginx UI: nginx config directive denylist (ValidateConfigFile) is bypassable via same-line ";" separation → authenticated directive injection (RCE on the official image) | High | Also affects github.com/0xJacky/Nginx-UI |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor