Resumen

Durante la auditoría de una aplicación web con autenticación en dos factores (2FA) basada en códigos OTP enviados por email, se descubrió que el sistema de rate limiting y generación de códigos trataba la dirección de email de forma case-sensitive, mientras que el protocolo SMTP entrega los emails de forma case-insensitive (segun RFC 5321).

Esto significaba que user@target.com, User@target.com, USER@target.com y cualquier otra variación de case generaban códigos OTP independientes con rate limits separados, pero todos llegaban al mismo buzón de correo del atacante o la víctima.

Detalles técnicos

Comportamiento normal

El flujo de autenticación era: login con email + password, y luego un código OTP de 6 dígitos enviado al email. El rate limit era de 3 intentos por código y 1 código cada 60 segundos por email.

Request OTP normal
POST /api/auth/request-otp HTTP/1.1
Content-Type: application/json

{ "email": "victim@company.com" }

→ 200 OK — Código OTP enviado (rate limit: 1/min para este email)

Bypass mediante variaciónes de case

Cada variación de case se trataba como un email distinto a nivel de aplicación, generando un nuevo código OTP con su propio rate limit:

Múltiples peticiones con case variations
POST /api/auth/request-otp  { "email": "victim@company.com" }    → Código A
POST /api/auth/request-otp  { "email": "Victim@company.com" }    → Código B
POST /api/auth/request-otp  { "email": "VICTIM@company.com" }    → Código C
POST /api/auth/request-otp  { "email": "vIctim@company.com" }    → Código D
POST /api/auth/request-otp  { "email": "viCtim@company.com" }    → Código E
...

Todos los códigos llegan al mismo buzón: victim@company.com
Cada variación tiene su propio rate limit de 3 intentos

Impacto en el brute force

Con un email de 6 caracteres en la parte local, existen 2^6 = 64 variaciónes de case posibles. Cada una genera un código OTP valido con 3 intentos:

Cálculo de probabilidades
Código OTP: 6 dígitos → 1.000.000 combinaciónes posibles
Rate limit normal: 3 intentos / código = 0.0003% probabilidad

Con bypass:
- 64 variaciónes de case × 1 código cada una = 64 códigos válidos
- Cualquier código es valido para la misma cuenta
- 64 códigos × 3 intentos = 192 intentos totales
- Con 64 códigos válidos de 6 dígitos:
  P(éxito) = 1 - (999936/1000000)^192 ≈ 1.2%

Repitiendo el proceso cada 60 segundos:
- En 10 minutos: P(éxito) ≈ 11.5%
- En 1 hora: P(éxito) ≈ 52%
Nota: Si el atacante tenía acceso al buzón de email de la víctima (por ejemplo, a través de una sesión comprometida o forwarding), podia ver todos los códigos generados y usarlos directamente sin necesidad de brute force, bypasseando completamente el 2FA.

Impacto

  • Bypass del mecanismo 2FA mediante multiplicación de códigos válidos
  • Evasión de rate limiting al generar identidades distintas para el mismo buzón
  • Acceso no autorizado a cuentas protegidas con OTP por email
  • Flood del buzón de la víctima con múltiples emails de código OTP

Remediación

  1. Normalizar el email a minúsculas antes de cualquier operación — email = email.strip().lower()
  2. Rate limiting por email normalizado — Aplicar limites sobre la version canonicalizada del email.
  3. Limitar códigos OTP activos — Invalidar el código anterior al generar uno nuevo para el mismo buzón.
  4. Implementar lockout progresivo — Bloquear la cuenta temporalmente tras N intentos fallidos acumulados.
  5. Considerar TOTP — Migrar a códigos basados en tiempo (Google Authenticator) que no dependen del email.

Referencias