CVE-2026-107808
Nginx UI: Authentication bypass: password login does not enforce a passkey-only second factor (2FA bypass)
Nginx UI is a web user interface for the Nginx web server. From 2.0.0 until 2.5.0, POST /api/login checks EnabledOTP but does not require a WebAuthn assertion when EnabledPasskey is true and no TOTP secret is configured. A passkey-only account is therefore issued a session after password verification, despite Enabled2FA reporting that the account has a second factor. An attacker who obtains the password can take over the account and reach administrative functionality without the registered passkey. This issue is fixed in version 2.5.0.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- High
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- High
- Availability
- High
Exploit likelihood
What each source reports
CVE List Score used
8.1
Nginx UI: Authentication bypass: password login does not enforce a passkey-only second factor (2FA bypass)
Nginx UI is a web user interface for the Nginx web server. From 2.0.0 until 2.5.0, POST /api/login checks EnabledOTP but does not require a WebAuthn assertion when EnabledPasskey is true and no TOTP secret is configured. A passkey-only account is therefore issued a session after password verification, despite Enabled2FA reporting that the account has a second factor. An attacker who obtains the password can take over the account and reach administrative functionality without the registered passkey. This issue is fixed in version 2.5.0.
- Record
CVE-2026-107808- CVSS 3.1
- 8.1 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H - Weakness
- CWE-287, CWE-305, CWE-308
- Updated
- 2026-11-02
EUVDEUVD-2026-95891
8.1
Nginx UI is a web user interface for the Nginx web server. From 2.0.0 until 2.5.0, POST /api/login checks EnabledOTP but does not require a WebAuthn assertion when EnabledPasskey is true and no TOTP secret is configured. A passkey-only account is therefore issued a session after password verification, despite Enabled2FA reporting that the account has a second factor. An attacker who obtains the password can take over the account and reach administrative functionality without the registered passkey. This issue is fixed in version 2.5.0.
- Record
EUVD-2026-95891- CVSS 3.1
- 8.1 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-45gv-9wjv-xh7p
8.1
Nginx UI: Authentication bypass: password login does not enforce a passkey-only second factor (2FA bypass)
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…
- Record
GHSA-45gv-9wjv-xh7p- CVSS 3.1
- 8.1 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H - Weakness
- CWE-287, CWE-305, CWE-308
- Updated
- 2026-11-02
NVD
8.1
Nginx UI is a web user interface for the Nginx web server. From 2.0.0 until 2.5.0, POST /api/login checks EnabledOTP but does not require a WebAuthn assertion when EnabledPasskey is true and no TOTP secret is configured. A passkey-only account is therefore issued a session after password verification, despite Enabled2FA reporting that the account has a second factor. An attacker who obtains the password can take over the account and reach administrative functionality without the registered passkey. This issue is fixed in version 2.5.0.
- Record
CVE-2026-107808- CVSS 3.1
- 8.1 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H - Weakness
- CWE-287, CWE-305, CWE-308
- Updated
- 2026-11-02
OSV
8.1
Nginx UI: Authentication bypass: password login does not enforce a passkey-only second factor (2FA bypass)
Nginx UI is a web user interface for the Nginx web server. From 2.0.0 until 2.5.0, POST /api/login checks EnabledOTP but does not require a WebAuthn assertion when EnabledPasskey is true and no TOTP secret is configured. A passkey-only account is therefore issued a session after password verification, despite Enabled2FA reporting that the account has a second factor. An attacker who obtains the password can take over the account and reach administrative functionality without the registered passkey. This issue is fixed in version 2.5.0.
- Record
CVE-2026-107808- CVSS 3.1
- 8.1 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H - Weakness
- CWE-287, CWE-305, CWE-308
- Updated
- 2026-11-02
OSVGHSA-45gv-9wjv-xh7p
8.1
Nginx UI: Authentication bypass: password login does not enforce a passkey-only second factor (2FA bypass)
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…
- Record
GHSA-45gv-9wjv-xh7p- CVSS 3.1
- 8.1 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H - Weakness
- CWE-287, CWE-305, CWE-308
- Updated
- 2026-11-02
OSVGO-2026-6704 No score
Nginx UI: Authentication bypass: password login does not enforce a passkey-only second factor (2FA bypass) in github.com/0xJacky/Nginx-UI
Nginx UI: Authentication bypass: password login does not enforce a passkey-only second factor (2FA bypass) in github.com/0xJacky/Nginx-UI
- Record
GO-2026-6704- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
github.com/0xJacky/Nginx-UIGo |
1.9.10-0.20250517140552-daee3ac7ade1 up to 1.9.10-0.20260728074558-95cd21b70814 | 1.9.10-0.20260728074558-95cd21b70814 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-107808 is a high vulnerability (CVSS 8.1) in Nginxui Nginx Ui, published October 9, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.41%, the 35th percentile. Fixed in github.com/0xJacky/Nginx-UI 1.9.10-0.20260728074558-95cd21b70814.
References
github.com/0xJacky/nginx-ui/security/advisories/GHSA-45gv-9wjv-xh7p
github.com/0xJacky/nginx-ui/commit/95cd21b70814e5d9a48a359aa238aeea1ac97429
github.com/0xJacky/nginx-ui/releases/tag/v2.5.0
github.com/advisories/GHSA-45gv-9wjv-xh7p
github.com/CVEProject/cvelistV5/tree/main/cves/2026/107xxx/CVE-2026-107808.json
nvd.nist.gov/vuln/detail/CVE-2026-107808
github.com/0xJacky/nginx-ui
Left out here: the rest of 2 source descriptions. The JSON record has them.