From 330c326fb52c83be0f36a32f2e4379d44a216661 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Wed, 16 Sep 2026 08:33:02 +0000 Subject: [PATCH] fix(mfa): key TOTP replay guard on time step, not the code --- server/internal/services/mfa.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/server/internal/services/mfa.go b/server/internal/services/mfa.go index fa6fa8d..ee778cd 100644 --- a/server/internal/services/mfa.go +++ b/server/internal/services/mfa.go @@ -8,6 +8,7 @@ import ( "encoding/base32" "encoding/hex" "errors" + "strconv" "strings" "time" @@ -289,16 +290,19 @@ func ClearMFA(instanceID, userID string) error { return err } -// burnTOTPCode makes a code single-use for 90 seconds - longer than the +-1 -// step window it could still validate in. Redis is already required for -// sessions. +// burnTOTPCode makes a step single-use for 90 seconds - longer than the +-1 +// step window it could still validate in. Keyed on the time step the code +// was accepted against, never on the code itself: a raw code sitting in a +// Redis key name would be a currently-valid credential readable by anything +// that can list keys. Redis is already required for sessions. func burnTOTPCode(userID, code string) error { if RedisClient == nil { return nil } ctx, cancel := mfaCtx() defer cancel() - key := "km:totp:" + userID + ":" + code + step := time.Now().Unix() / 30 + key := "km:totp:" + userID + ":" + strconv.FormatInt(step, 10) ok, err := RedisClient.SetNX(ctx, key, 1, 90*time.Second).Result() if err != nil { return err