VYPR
Medium severity5.9NVD Advisory· Published Apr 10, 2026· Updated Apr 17, 2026

CVE-2026-35597

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.

PackageAffected versionsPatched versions
code.vikunja.io/apiGo
< 2.3.02.3.0

Affected products

1

Patches

1
6ca0151d02fa

test(webtests): add end-to-end TOTP lockout test

https://github.com/go-vikunja/vikunjakolaenteApr 9, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.