# Nginx UI: TOTP / passkey enrolment requires no re-authentication, enabling persistent account takeover

Nginx UI TOTP/passkey enrolment endpoints sit outside RequireSecureSession, so a stolen JWT lets an attacker bind their own 2FA to the victim's account.

- Payout: Research
- Severity: high
- Program: Nginx UI
- Found by: therawdev
- Disclosed: 2026-10-15
- CVE: CVE-2026-84313
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-gfrc-g87h-7h5w
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-gfrc-g87h-7h5w
- Tags: area:web domain:api domain:web impact:ato impact:privesc kind:research platform:ghsa sev:high tech:go technique:source-review vuln:access-control vuln:account-takeover vuln:auth-bypass vuln:mfa-bypass

**Target:** Go web API (Nginx UI user/2FA endpoints)
**Root cause:** Enrolment routes /api/otp_enroll and /api/finish_passkey_register are registered outside the RequireSecureSession middleware group and require only a valid JWT, with no current-password re-prompt. EnrollTOTP only rejects when OTP is already enabled, and FinishPasskeyRegistration has no already-enrolled guard at all.
**Steps:**
1. Obtain victim JWT.
2. `curl -s -H "Authorization: $JWT" http://127.0.0.1:18080/api/otp_secret` returns attacker-chosen secret.
3. Compute current 30s TOTP code from that secret (python hmac snippet in advisory).
4. `curl -X POST -H "Authorization: $JWT" -H "Content-Type: application/json" -d '{"secret":"5BWZDEWWECJ6LRPSDKHURCWXBU5KKRYN","passcode":"954084"}' http://127.0.0.1:18080/api/otp_enroll` returns 200 with recovery codes.
5. `curl -s -H "Authorization: $JWT" http://127.0.0.1:18080/api/2fa_status` confirms enabled:true.
6. Passkey variant: `GET /api/begin_passkey_register` then `POST /api/finish_passkey_register` with attacker authenticator.
**How found:** Code review of api/user/router.go: noticed /otp_enroll and passkey registration routes registered before the RequireSecureSession group while /otp_reset was inside it — an inconsistency that flagged missing re-auth on critical 2FA mutations.
**Exploitation:** With only a stolen JWT, attacker calls /api/otp_secret to get a secret, computes a valid TOTP, and POSTs it to /api/otp_enroll, binding their authenticator to the victim account. Victim's future logins require the attacker's second factor; password rotation does not remove it. Passkey registration similarly lets attacker add their own authenticator, with no already-enrolled guard.
**Escalation:** Chains with stolen JWT, the RequireSecureSession bypass (middleware no-op for non-OTP users), missing-role-model privilege escalation, and weak recovery-code generation.
**Insight:** When auditing 2FA enrolment, compare route registration order against the secure-session middleware group: reset endpoints are often protected while enrol endpoints are not, and enrolment is the more dangerous primitive since it survives password resets.

