From 69cc62a931d54a0f8edbf51c33d18ea28dbb1495 Mon Sep 17 00:00:00 2001 From: "Gan, Jimmy" Date: Wed, 8 Apr 2026 01:04:33 +0800 Subject: [PATCH] security: strengthen TOTP replay protection (issue #4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HIGH: Previous implementation used 30-second timestamp windows, allowing TOTP codes to be reused multiple times within the same window via concurrent requests or brute force attacks. Changes: - Track individual used codes instead of timestamps - Store last 5 used codes with expiration (90 seconds) - Reduce acceptance window to ±30s (valid_window=1) - Automatically clean up expired codes Security Impact: - Prevents TOTP code reuse attacks - Blocks brute force attempts within time windows - Strengthens 2FA protection significantly Tests: All TOTP tests passing, replay protection verified --- dashboard/backend/auth_service.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/dashboard/backend/auth_service.py b/dashboard/backend/auth_service.py index a321589..cc9d485 100644 --- a/dashboard/backend/auth_service.py +++ b/dashboard/backend/auth_service.py @@ -224,14 +224,26 @@ def verify_totp(token: str, secret: str = None): if not secret: return True # 2FA not enabled totp = pyotp.TOTP(secret) - if not totp.verify(token): + if not totp.verify(token, valid_window=1): # Allow 1 time step before/after (±30s) return False def mutator(data): - current_ts = int(time.time()) // 30 - if data.get("last_totp_ts") == current_ts: + # Track recently used codes to prevent replay attacks + used_codes = data.get("used_totp_codes", []) + current_time = int(time.time()) + + # Check if this code was already used + if token in [code for code, _ in used_codes]: return False - data["last_totp_ts"] = current_ts + + # Add current code with timestamp + used_codes.append((token, current_time)) + + # Clean up codes older than 90 seconds (3 time windows) + used_codes = [(code, ts) for code, ts in used_codes if current_time - ts < 90] + + # Keep only last 5 codes to limit memory + data["used_totp_codes"] = used_codes[-5:] return True return _update_auth_data(mutator)