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.
This commit is contained in:
kolaente 2026-03-20 10:12:43 +01:00 committed by kolaente
parent 5591ca94ba
commit 5f06e1dce5
2 changed files with 19 additions and 1 deletions

View File

@ -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
}

View File

@ -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)