{"schema":"threatline.record/2","record":{"id":"CVE-2026-107808","aliases":["GHSA-45gv-9wjv-xh7p","GO-2026-6704"],"summary":"Nginx UI: Authentication bypass: password login does not enforce a passkey-only second factor (2FA bypass)","details":"## Summary\n\nnginx-ui supports two second-factor methods — TOTP (OTP) and WebAuthn passkeys —\nand reports an account as 2FA-enabled when **either** is configured. However, the\npassword login endpoint (`POST /api/login`) only enforces a second factor when a\n**TOTP secret** is present. An account that has registered a **passkey but no\nTOTP** is logged in after password verification alone — the passkey is never\nrequested. This silently downgrades a passkey-protected account to single-factor\n(password-only) authentication.\n\n## Details\n\nThe account's 2FA policy treats passkeys as a valid factor (`model/user.go`):\n\n```go\nfunc (u *User) EnabledOTP() bool     { return len(u.OTPSecret) != 0 }\nfunc (u *User) EnabledPasskey() bool { /* true if a passkey row exists */ }\nfunc (u *User) Enabled2FA() bool     { return u.EnabledOTP() || u.EnabledPasskey() }\n\nfunc (u *User) AfterFind(_ *gorm.DB) error {\n    u.EnabledTwoFA = u.Enabled2FA() // exposed to the UI as `enabled_2fa`\n    return nil\n}\n```\n\nBut the login handler only checks `EnabledOTP()` (`api/user/auth.go`, `Login`):\n\n```go\nu, err := user.Login(json.Name, json.Password)\n...\nif u.EnabledOTP() {                       // <-- only TOTP is enforced\n    if json.OTP == \"\" && json.RecoveryCode == \"\" {\n        c.JSON(http.StatusOK, LoginResponse{Message: \"The user has enabled 2FA\", Code: Enabled2FA}) // 199\n        user.BanIP(clientIP)\n        return\n    }\n    if _, err = user.VerifyOTP(u, json.OTP, json.RecoveryCode); err != nil { /* ... */ }\n    secureSessionID = user.SetSecureSessionID(u.ID)\n}\n\n// Passkey-only accounts fall through to here and receive a full session token:\naccessToken, err := user.GenerateJWT(u)\n```\n\nNo branch requires a WebAuthn assertion during password login when\n`EnabledPasskey()` is true. The root cause is the mismatch between the\n**policy definition** (`Enabled2FA()` = OTP **or** passkey) and the\n**enforcement check** (`EnabledOTP()` only).\n\nThe same `EnabledOTP()`-only gating in `RequireSecureSession()`\n(`internal/middleware/secure_session.go`) means passkey-only users are also\nexempted from step-up on sensitive actions.\n\n## Proof of Concept\n\nTested against nginx-ui built from source (`go build -tags unembed`) on\n`127.0.0.1:9000`, with WebAuthn configured and a victim account that has a\nregistered passkey and **no** TOTP. The vulnerable code is identical on the\nproduction `main` branch (`api/user/auth.go` `Login` gates on `u.EnabledOTP()`\nonly; verified at commit `6c86e5a`, 2026-05-17).\n\nAccount state advertised by `GET /api/2fa_status`:\n\n```json\n{\"enabled\":true,\"otp_status\":false,\"passkey_status\":true, ...}\n```\n\nAttacker logs in with **password only** (`POST /api/login`, encrypted params as\nthe client normally sends):\n\n```\nHTTP 200\n{\"message\":\"ok\",\"code\":200,\"token\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....\"}\n```\n\n`code: 200` + a valid JWT = an authenticated session, with the passkey never\nused. For comparison, the identical account with a TOTP secret instead correctly\nreturns the second-factor challenge:\n\n```\nHTTP 200\n{\"message\":\"The user has enabled 2FA\",\"code\":199}\n```\n\n| Account state | `POST /api/login` (password only) |\n|---|---|\n| Passkey registered, no TOTP | `code 200` + JWT — **2FA NOT enforced** |\n| TOTP registered | `code 199` — 2FA challenge enforced |\n\nThis isolates the defect: the login path enforces OTP but ignores passkeys.\n\n## Impact\n\n- Any account protected **only** by a passkey is reduced to password-only\n  authentication. An attacker who obtains the password (phishing, reuse, leak)\n  gains full access despite the registered security key.\n- In nginx-ui all authenticated users are effectively administrators and the\n  terminal feature grants a host shell, so account takeover leads to full\n  control of the managed nginx instance / host.\n- Users are given a false sense of security: the UI shows the account as\n  2FA-enabled while the second factor is not enforced at login.\n\n## Remediation\n\n- Gate the second-factor decision on `u.Enabled2FA()` (not `u.EnabledOTP()`) in\n  `Login`, in both SSO callbacks, and in `RequireSecureSession()`.\n- For a passkey-only user logging in with a password, return a\n  \"passkey assertion required\" challenge and complete login only after a\n  successful WebAuthn assertion (the `begin_passkey_login` / `finish_passkey_login`\n  flow already exists and should be required as the second step).\n- Centralize session-token issuance so no login entry point can skip the 2FA\n  decision.\n\n## Affected components\n\n- `api/user/auth.go` — `Login`\n- `model/user.go` — `EnabledOTP`, `EnabledPasskey`, `Enabled2FA`, `AfterFind`\n- `api/user/2fa.go` — `get2FAStatus`\n- `internal/middleware/secure_session.go` — `RequireSecureSession`","categories":["Web servers"],"product":"github.com/0xJacky/Nginx-UI","vendor":"0xJacky","ecosystem":"go","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-45gv-9wjv-xh7p","source_modified_at":"2026-10-09T17:08:16Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"go","name":"github.com/0xJacky/Nginx-UI"},"range":">= 1.9.10-0.20250517140552-daee3ac7ade1, < 1.9.10-0.20260728074558-95cd21b70814"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-107808","source_modified_at":"2026-10-09T17:19:53.438Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"nginx-ui","vendor":"0xJacky","versions":[{"version":">= 2.0.0, < 2.5.0","status":"affected"}],"defaultStatus":null},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-45gv-9wjv-xh7p","source_modified_at":"2026-10-19T21:41:12.332793483Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"Go","name":"github.com/0xJacky/Nginx-UI","purl":"pkg:golang/github.com/0xJacky/Nginx-UI"},"ranges":[{"events":[{"introduced":"1.9.10-0.20250517140552-daee3ac7ade1"},{"fixed":"1.9.10-0.20260728074558-95cd21b70814"}],"type":"SEMVER"}],"versions":[]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-45gv-9wjv-xh7p","source_modified_at":"2026-10-09T17:08:16Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"go","name":"github.com/0xJacky/Nginx-UI"},"version":"1.9.10-0.20260728074558-95cd21b70814"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-45gv-9wjv-xh7p","source_modified_at":"2026-10-19T21:41:12.332793483Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"Go","name":"github.com/0xJacky/Nginx-UI","purl":"pkg:golang/github.com/0xJacky/Nginx-UI"},"version":"1.9.10-0.20260728074558-95cd21b70814"}],"cvss":8.1,"severity":"High","kev":{"listed":false},"epss":{"probability":0.00413,"percentile":0.34451,"score_date":"2026-10-23"},"published_at":"2026-10-09T15:11:44.387Z","modified_at":"2026-10-19T21:41:12.332793483Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-45gv-9wjv-xh7p","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-09T17:08:16Z"},{"source":"CVE List","url":"https://www.cve.org/CVERecord?id=CVE-2026-107808","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-09T17:19:53.438Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-45gv-9wjv-xh7p","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-19T21:41:12.332793483Z"},{"source":"EPSS","url":"https://api.first.org/data/v1/epss?cve=CVE-2026-107808","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-23"}],"sources":["GitHub Advisory Database","CVE List","OSV","EPSS"],"references":[{"source":"CVE List","url":"https://github.com/0xJacky/nginx-ui/security/advisories/GHSA-45gv-9wjv-xh7p","tags":["exploit"]},{"source":"CVE List","url":"https://github.com/0xJacky/nginx-ui/commit/95cd21b70814e5d9a48a359aa238aeea1ac97429","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/0xJacky/nginx-ui/releases/tag/v2.5.0","tags":["x_refsource_MISC"]},{"url":"https://github.com/advisories/GHSA-45gv-9wjv-xh7p","source":"GitHub Advisory Database"}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"cve_state":"PUBLISHED","ssvc":{"exploitation":"poc","automatable":"no","technical_impact":"total","assessed_at":"2026-10-09T17:16:59.141852Z"},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"product","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-19T21:41:12.332793483Z","content_hash":"11d393dbae450febeeab9510b06a125f3fce78eeb30accb5984bb82404b39f13"}}
