← Todas las noticias
security5 min de lectura

The TOTP mistakes AI-generated MFA code makes

Por Seaworthy · 7 de julio de 2026

Red padlock resting on a black computer keyboard, representing authentication security

Photo by FlyD on Unsplash

The happy path is not the security path

AI tools are genuinely good at TOTP setup. Ask for "add Google Authenticator support to my Express app" and you'll get working QR code generation, a /verify endpoint, a database field for the secret. The code compiles. The happy path works.

The problem is that security lives in the unhappy path. An attacker is not going to scan a QR code and type in their code politely. Here are four things AI-generated TOTP code almost always gets wrong.

No rate limiting on /verify

This is the obvious one, and it gets missed constantly. A TOTP code is six digits. That's 1,000,000 possible values. Without rate limiting, an attacker can brute-force every possible code against a single account.

How long does that take? With a simple POST request and no throttling, even a modest attack script can fire 100 requests per second. At that rate, you exhaust all 1,000,000 codes in under three hours. On a fast server with no queuing, it's faster. And the attacker does not need to try every code: they only need to hit the one or two valid codes active at any given moment, so the window for a successful hit is much wider than it looks.

The fix is to rate limit by user ID or account identifier, not by IP. IP-based rate limiting is trivially bypassed with proxies.

// Bad: no rate limit
app.post('/verify', async (req, res) => {
  const { userId, token } = req.body;
  const user = await db.users.findOne({ id: userId });
  const valid = totp.verify({ token, secret: user.totpSecret });
  res.json({ valid });
});

// Better: rate limit by userId
import { RateLimiterMemory } from 'rate-limiter-flexible';

const limiter = new RateLimiterMemory({ points: 5, duration: 60 });

app.post('/verify', async (req, res) => {
  try {
    await limiter.consume(req.body.userId);
  } catch {
    return res.status(429).json({ error: 'Too many attempts' });
  }
  // ... rest of handler
});

Five attempts per 60 seconds is generous for a legitimate user and brutal for an attacker.

No replay protection

A TOTP code is valid for 30 seconds. Many implementations allow a one-step clock drift window, making the effective validity window 90 seconds. Within that window, the same code can be submitted multiple times, and AI-generated code almost never accounts for this.

A compromised log file, a shoulder surfer, or a phishing page that forwards codes in real time can all reuse a code within its validity window. The fix is to record used codes. After a successful verification, store the code alongside the user ID and reject any combination you have seen before, even if the TOTP library would otherwise accept it.

// production: use Redis with a 90-second TTL instead of a Map
const usedCodes = new Map();

async function verifyTotp(userId, token, secret) {
  const codeKey = `${userId}:${token}`;
  if (usedCodes.has(codeKey)) {
    return false; // replay attempt
  }
  const valid = totp.verify({ token, secret, window: 1 });
  if (valid) {
    usedCodes.set(codeKey, true);
  }
  return valid;
}

In production, use Redis with a 90-second TTL so you are not holding codes forever and the check stays fast under load.

Weak secret generation

AI models often reach for Math.random() to generate TOTP secrets, or they use crypto.randomBytes() with a buffer that is too small. Math.random() is not cryptographically secure. Its output is predictable if an attacker can observe enough values from the same process.

TOTP secrets are base32-encoded. RFC 4226 recommends at least 160 bits of entropy. That means at least 20 bytes from a cryptographically secure source.

// Bad: not cryptographically secure
const secret = Math.random().toString(36).substring(2);

// Also bad: too short (only 64 bits)
const secret = crypto.randomBytes(8).toString('base64');

// Good: 20 bytes = 160 bits, encoded as base32
import crypto from 'crypto';
import { encode } from 'hi-base32';

function generateTotpSecret() {
  return encode(crypto.randomBytes(20)).replace(/=/g, '');
}

Storing the secret in plaintext

The TOTP secret is a long-term credential. If your database is compromised and the secrets are in plaintext, an attacker can generate valid TOTP codes for every account, indefinitely. It completely defeats the second factor.

AI code almost always stores the secret as a plain string column. The correct approach is to encrypt it at rest using a server-side key that lives in an environment variable or a secrets manager, not in the database itself.

import crypto from 'crypto';

const ENCRYPTION_KEY = Buffer.from(process.env.TOTP_ENCRYPTION_KEY, 'hex'); // 32 bytes
const IV_LENGTH = 16;

function encrypt(text) {
  const iv = crypto.randomBytes(IV_LENGTH);
  const cipher = crypto.createCipheriv('aes-256-cbc', ENCRYPTION_KEY, iv);
  const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
  return iv.toString('hex') + ':' + encrypted.toString('hex');
}

function decrypt(text) {
  const [ivHex, encryptedHex] = text.split(':');
  const iv = Buffer.from(ivHex, 'hex');
  const encrypted = Buffer.from(encryptedHex, 'hex');
  const decipher = crypto.createDecipheriv('aes-256-cbc', ENCRYPTION_KEY, iv);
  return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString();
}

Store the result of encrypt(secret) in the database. Decrypt only at verification time.

The pattern

Each of these mistakes is individually fixable, and none requires a library change or a new framework. The problem is that AI tools optimise for working code, not secure code, and TOTP security is almost entirely in the edge cases that do not appear in a basic demo.

Seaworthy flags missing TOTP rate limiting and plaintext secret storage as part of its security checks.

Este artículo fue generado por IA y resume fuentes públicamente disponibles.

De los creadores de Seaworthy

Seaworthy analiza tu repositorio en busca de los problemas que se tratan en artículos como este.

Brechas de seguridad, secretos expuestos y configuraciones erróneas detectados antes de desplegar. Gratis, sin cuenta necesaria.

$ npx seaworthycode