fix(security): prevent TOTP replay attacks and fix user enumeration in verifyTotp
Adds lastTotpAt timestamp to User model. After a successful TOTP validation, the timestamp is recorded. Any reuse of the same code within the 30-second window is rejected as a replay attack. verifyTotp now returns a single generic UNAUTHORIZED error regardless of whether the user ID is invalid or TOTP is not enabled, preventing enumeration of user IDs and MFA status. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -146,6 +146,35 @@ const config = {
|
||||
});
|
||||
throw new InvalidTotpError();
|
||||
}
|
||||
|
||||
// Replay-attack prevention: reject if the same 30-second window was already used
|
||||
const userWithTotp = await prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
select: { lastTotpAt: true },
|
||||
}) as { lastTotpAt: Date | null } | null;
|
||||
if (
|
||||
userWithTotp?.lastTotpAt != null &&
|
||||
Date.now() - userWithTotp.lastTotpAt.getTime() < 30_000
|
||||
) {
|
||||
logger.warn({ email, reason: "totp_replay" }, "TOTP replay attack blocked");
|
||||
void createAuditEntry({
|
||||
db: prisma,
|
||||
entityType: "Auth",
|
||||
entityId: user.id,
|
||||
entityName: user.email,
|
||||
action: "CREATE",
|
||||
userId: user.id,
|
||||
summary: "Login failed — TOTP replay detected",
|
||||
source: "ui",
|
||||
});
|
||||
throw new InvalidTotpError();
|
||||
}
|
||||
|
||||
// Record successful TOTP use to prevent replay within the same window
|
||||
await (prisma.user.update as Function)({
|
||||
where: { id: user.id },
|
||||
data: { lastTotpAt: new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
// MFA enforcement: if the user's role is in requireMfaForRoles but they
|
||||
|
||||
Reference in New Issue
Block a user