From 5f06e1dce56ca2b1845c9adb7aacab8777296e1f Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 20 Mar 2026 10:12:43 +0100 Subject: [PATCH] fix: prevent TOTP passcode reuse within validity window Store used TOTP passcodes in the keyvalue store after successful validation. On subsequent validation attempts, check if the passcode was already used for the same user and reject it with ErrTOTPPasscodeUsed. This prevents replay attacks where an intercepted TOTP code could be reused within its 30-second validity window. --- pkg/user/totp.go | 18 ++++++++++++++++++ pkg/user/totp_test.go | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pkg/user/totp.go b/pkg/user/totp.go index 2a270deb1..31235cce5 100644 --- a/pkg/user/totp.go +++ b/pkg/user/totp.go @@ -17,7 +17,9 @@ package user import ( + "fmt" "image" + "strconv" "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/log" @@ -136,6 +138,22 @@ func ValidateTOTPPasscode(s *xorm.Session, passcode *TOTPPasscode) (t *TOTP, err return nil, ErrInvalidTOTPPasscode{Passcode: passcode.Passcode} } + // Prevent passcode reuse: check if this passcode was already used + usedKey := fmt.Sprintf("totp_used_%s_%s", strconv.FormatInt(passcode.User.ID, 10), passcode.Passcode) + _, exists, err := keyvalue.Get(usedKey) + if err != nil { + return nil, err + } + if exists { + return nil, ErrTOTPPasscodeUsed{} + } + + // Mark this passcode as used + err = keyvalue.Put(usedKey, true) + if err != nil { + return nil, err + } + return } diff --git a/pkg/user/totp_test.go b/pkg/user/totp_test.go index be3279adb..0a4722ca9 100644 --- a/pkg/user/totp_test.go +++ b/pkg/user/totp_test.go @@ -33,7 +33,7 @@ func TestTOTPPasscodeCannotBeReused(t *testing.T) { defer s.Close() // Generate a valid TOTP passcode for user1's secret from the fixture - secret := "JBSWY3DPEHPK3PXP" + secret := "JBSWY3DPEHPK3PXP" //nolint:gosec passcode, err := totp.GenerateCode(secret, time.Now()) require.NoError(t, err)