CVE-2026-35597
Description
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.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
code.vikunja.io/apiGo | < 2.3.0 | 2.3.0 |
Affected products
1Patches
16ca0151d02fatest(webtests): add end-to-end TOTP lockout test
1 file changed · +47 −0
pkg/webtests/login_test.go+47 −0 modified@@ -19,10 +19,13 @@ package webtests import ( "net/http" "testing" + "time" + "code.vikunja.io/api/pkg/db" apiv1 "code.vikunja.io/api/pkg/routes/api/v1" "code.vikunja.io/api/pkg/user" + "github.com/pquerna/otp/totp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -66,3 +69,47 @@ func TestLogin(t *testing.T) { assertHandlerErrorCode(t, err, user.ErrCodeEmailNotConfirmed) }) } + +func TestLoginTOTPLockout(t *testing.T) { + // user10 fixture: TOTP secret JBSWY3DPEHPK3PXP, password 12345678. + const totpSecret = "JBSWY3DPEHPK3PXP" //nolint:gosec + + // Share one env across requests: setupTestEnv re-inits keyvalue on each + // call, so using newTestRequest would reset the attempt counter every + // iteration and the lockout would never trigger. + e, err := setupTestEnv() + require.NoError(t, err) + + invalidPayload := `{ + "username": "user10", + "password": "12345678", + "totp_passcode": "000000" +}` + + for i := 0; i < 11; i++ { + c, _ := createRequest(e, http.MethodPost, invalidPayload, nil, nil) + err := apiv1.Login(c) + require.Error(t, err) + } + + s := db.NewSession() + locked := &user.User{} + exists, err := s.Where("id = ?", 10).Get(locked) + require.NoError(t, err) + require.True(t, exists) + require.NoError(t, s.Close()) + assert.Equal(t, user.StatusAccountLocked, locked.Status, + "user10 should be locked after 10 failed TOTP attempts") + + validCode, err := totp.GenerateCode(totpSecret, time.Now()) + require.NoError(t, err) + validPayload := `{ + "username": "user10", + "password": "12345678", + "totp_passcode": "` + validCode + `" +}` + c, _ := createRequest(e, http.MethodPost, validPayload, nil, nil) + err = apiv1.Login(c) + require.Error(t, err) + assertHandlerErrorCode(t, err, user.ErrCodeAccountLocked) +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/go-vikunja/vikunja/commit/6ca0151d02fa0e8c7e2181ab916a28e08caaaec8nvdPatchWEB
- github.com/go-vikunja/vikunja/security/advisories/GHSA-fgfv-pv97-6cmjnvdExploitVendor AdvisoryWEB
- github.com/advisories/GHSA-fgfv-pv97-6cmjghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-35597ghsaADVISORY
- github.com/go-vikunja/vikunja/pull/2576nvdIssue TrackingWEB
- github.com/go-vikunja/vikunja/releases/tag/v2.3.0nvdRelease NotesWEB
News mentions
0No linked articles in our index yet.