Skip to content

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)

High Go
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() (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

Affected versions

Go

PackageVulnerableFixed 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

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy 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
Is this in your code?

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