El signo menos que generó dinero: business logic bypass en una fintech
Submitir un monto negativo en el flujo de créditos no fallaba: hacía que el balance aumentara. Cómo una fórmula mal diseñada permitía inflar saldos arbitrariamente y por qué los escáneres jamás encontrarán esto.
El hallazgo en una línea
En el flujo de “Aplicar como crédito” de una plataforma fintech, enviar un monto negativo como -24.44 no era rechazado por el servidor. La consecuencia: el saldo total mostrado aumentaba en $24.44 en lugar de disminuir.
Una operación tan simple permitió inflar el saldo disponible hasta +$900,000 en el entorno de pruebas. Sin tocar las credenciales, sin escalar privilegios, sin explotar ninguna vulnerabilidad técnica clásica. Solo un signo menos.
Contexto del engagement
El alcance era una API REST que maneja el flujo de presales y créditos para empresas mayoristas. Estaba en staging cuando lo evaluamos y a punto de promoverse a producción.
El equipo de desarrollo confiaba en el frontend para validar los montos. Un selector con steppers de +/− que solo permite valores positivos y un campo numérico que rechaza decimales con regex. Lo típico.
Reconocimiento
El proceso fue trivial:
- Login con un usuario válido
- Navegar a la sección de “Bulk Presales”
- Click en “Apply as Credit”
- Ingresar un monto pequeño, por ejemplo
24.44
El frontend valida y muestra previa: “Saldo final: $277,919.10” (de un saldo inicial de $277,943.54).
Hasta aquí todo bien. La lógica esperada es: Saldo final = Saldo inicial − Monto.
Explotación
Intercepté la petición con Burp Suite y modifiqué el body antes de reenviarla:
POST /api/v2/presales/credit HTTP/1.1
Host: app.fintech-redacted.mx
Authorization: Bearer [REDACTED]
Content-Type: application/json
{
"type": "credit",
"amount": -24.44,
"presale_id": "[REDACTED]"
}
El servidor aceptó la petición con 200 OK y persistió el registro con amount: -24.44. Al recargar el dashboard, el balance mostraba $277,967.98.
La operación matemática real del backend era literalmente:
Saldo final = Saldo inicial − Monto
= 277,943.54 − (−24.44)
= 277,943.54 + 24.44
= 277,967.98
Un menos por un menos da más. Cinco minutos de exploit.
Escalando el impacto
Para confirmar que no había validación de magnitud, repetí la operación con -900000.00:
{
"type": "credit",
"amount": -900000.00,
"presale_id": "[REDACTED]"
}
El servidor también aceptó. El dashboard mostró un saldo inflado consistente con la inyección. Cero validaciones de signo, cero validaciones de magnitud, cero límites de operación.
En un entorno productivo conectado a procesos automatizados de pagos o conciliación, esto detona inmediatamente:
- Pagos fraudulentos a proveedores ficticios
- Corrupción de la conciliación contable
- Reclamos con el banco que tardarían semanas en rastrearse
- Posible fraude masivo si el endpoint expone llamadas concurrentes
Por qué un escáner nunca encuentra esto
Esta es la categoría de vulnerabilidad más invisible para herramientas automáticas:
Los escáneres prueban valores anómalos (XSS payloads, SQL injection, archivos grandes), pero no prueban lógica de negocio. No tienen forma de saber que un monto debe ser positivo, ni que la fórmula del backend invierte el signo cuando se le pasa un negativo. Esa información vive en la cabeza del desarrollador y en los requirements del producto.
Para encontrarlo, hay que entender el dominio: qué es un crédito, qué es un débito, qué dirección tiene la operación, dónde se aplica el signo, y qué tipos de input no cubrió la validación.
Remediación
La corrección es trivial y debe vivir en el servidor:
# Backend (middleware de validación)
def validate_credit_amount(amount: Decimal) -> None:
if amount <= 0:
raise ValidationError("El monto debe ser un valor positivo mayor a cero")
if amount > MAX_CREDIT_PER_TX:
raise ValidationError(f"El monto excede el límite de {MAX_CREDIT_PER_TX}")
Tres cosas siempre:
- Validar el signo según la dirección que la operación espera. Si es un crédito, ≥ 0. Si es un débito, ≤ 0. Hacerlo explícito.
- Validar la magnitud máxima por transacción y por ventana de tiempo. Que un fraude en producción tenga un techo.
- Aplicar la regla en el servidor, no solo en el frontend. El frontend valida UX; el servidor valida confianza.
Adicionalmente, cualquier operación monetaria debería tener:
- Auditoría persistente (quién, cuándo, monto, IP)
- Alertas en montos anómalos (más de N desviaciones estándar del promedio del usuario)
- Reconciliación periódica con un proceso secundario independiente
Conclusión
Este patrón aparece en al menos 3 de cada 10 fintechs que evaluamos en LATAM. La presión de salir a producción hace que la validación se quede del lado del cliente, donde “se ve más rápido”. El servidor confía y el frontend se puede modificar con cualquier proxy.
Cualquier operación que mueve dinero debe asumir que el cliente miente. Es la única hipótesis segura.
¿Tu plataforma maneja flujos transaccionales por API? Contáctanos para una evaluación de lógica de negocio enfocada a fintech.