Brute-force-friendly rate limit on 2FA TOTP verification endpoint
`POST /rest/2fa/verify` is rate-limited at 100 attempts per 5 minutes per IP with no per-account lockout, and the verifier uses `epochTolerance: 30` (≈30 minutes of valid codes); combined this is well within practical brute-force range of a 6-digit TOTP.
server.ts L451-454:
app.post('/rest/2fa/verify',
rateLimit({ windowMs: 5 * 60 * 1000, max: 100, validate: false }),
utils.asyncHandler(twoFactorAuth.verify)
)
The 2FA flow itself is otherwise correctly structured — routes/login.ts returns a partial tmpToken (typed password_valid_needs_second_factor_token) instead of a full session when totpSecret is set, and routes/2fa.ts L20-24 derives the user id from the signed token rather than req.body. The weakness is purely brute-force resistance:
- The bucket is 100 requests per 5 minutes (20/min). The brief considers anything above ~5/min on a 6-digit code brute-forceable.
app.enable('trust proxy')is set globally (L339) and the verify-2fa rate limiter does not pin itskeyGeneratorto anything stable, soX-Forwarded-Forcontrols the bucket key — an attacker rotates that header and the limit effectively disappears.- The limit is per-IP, not per
tmpToken.sub/ userId, so an attacker can rotate IPs against a single victim. routes/2fa.tsL31 verifies the supplied code withverifySync({ secret: user.totpSecret, token: totpToken, epochTolerance: 30 }).valid. InotplibepochToleranceis measured in 30-second windows, so 30 epochs widens the accepted-code set to ~61 codes per moment, shrinking the effective search space from 10^6 to ~1.6×10^4. At 20 attempts/minute that exhausts in ~13 hours from a single IP, and minutes with IP rotation.
The net effect: holder of a victim's password (e.g., obtained via the weak password policy or the SQL-injection login route at routes/login.ts L33) can brute-force the second factor by sending guesses to /rest/2fa/verify along with the tmpToken returned by /rest/user/login.
- Authenticate password-step against a 2FA-enabled account and capture the
tmpToken:
`` POST /rest/user/login {"email":"wurstbrot@juice-sh.op","password":"EinBelegtesBrotMitSchinkenSCHINKEN"} → 401 { status: "totp_token_required", data: { tmpToken: "..." } } ``
- Brute-force
/rest/2fa/verifyrotatingX-Forwarded-For:
`` for code in 000000..999999: POST /rest/2fa/verify X-Forwarded-For: <random IP> { "tmpToken": "...", "totpToken": "<code>" } ` Because the limiter is keyed off req.ip (derived from XFF under trust proxy`), each random XFF resets the bucket, and the 30-epoch tolerance means ≥61 of every 10⁶ codes succeed.
An attacker who knows the victim's password can defeat the second factor at scale, fully bypassing 2FA for any account. No additional authentication is required. Affects every endpoint protected by the 2FA-issued bearer token.
Lines 451-454 set rateLimit({ windowMs: 5*60*1000, max: 100, validate: false }) with no custom keyGenerator, and app.enable('trust proxy') is set globally above, so the limiter keys on req.ip derived from attacker-controlled X-Forwarded-For. The limit is per-IP rather than per tmpToken.sub, with no account lockout middleware, so password-holding attackers can rotate XFF to brute-force the 6-digit TOTP — especially given the wide epochTolerance in routes/2fa.ts cited by the detector. The exploit path (login → tmpToken → brute-force /rest/2fa/verify) is reachable from an unauthenticated network attacker once a password is known, defeating the second factor.
/rest/2fa/verify (L451-454) is reachable over the network with no authentication middleware — only a tmpToken issued by /rest/user/login is needed, so PR:N and UI:N. With app.enable('trust proxy') (L339) the limiter keys off req.ip, which is attacker-controlled via X-Forwarded-For, so the 100/5min cap is trivially bypassed; combined with epochTolerance: 30 (~61 accepted codes out of 10^6) the brute-force is mechanically straightforward (AC:L). The password prerequisite is a chained precondition (and the finding notes a SQLi login path) but the 2FA layer itself collapses without special conditions. Successful exploitation yields full account takeover (C:H/I:H) against any 2FA-enabled user, including high-value accounts; impact is confined to the app (S:U) and the bug isn't itself a DoS (A:N).
- CWE-307
- CWE-799
- OWASP ASVS 2.2.1