La consulta de búsqueda de transacciones construye SQL concatenando directamente el parámetro
merchantId proporcionado por el usuario en el cuerpo de la petición.
Un atacante puede inyectar SQL arbitrario para exfiltrar o destruir datos.
let getTransactionsByMerchant (db: DbContext) (merchantId: string) = let sql = $"SELECT * FROM transactions WHERE merchant_id = '{merchantId}'" db.Database.ExecuteSqlRaw(sql) // ⚠ SQL injection directo
let getTransactionsByMerchant (db: DbContext) (merchantId: MerchantId) = db.Transactions .Where(fun t -> t.MerchantId = merchantId.Value) .AsNoTracking() .ToListAsync() // Consulta parametrizada vía LINQ/EF Core
Usar LINQ o FromSqlInterpolated (no ExecuteSqlRaw) con parámetros tipados. Además envuelve merchantId en un tipo de dominio MerchantId (single-case DU) para evitar confusiones futuras.
El handler de descarga de recibos acepta el nombre de fichero desde el query string y lo concatena
directamente al directorio base sin validar que el path resultante esté dentro del directorio permitido.
Permite GET /receipts?file=../../appsettings.json.
let baseDir = "/var/cultivapay/receipts" let filePath = Path.Combine(baseDir, fileName) // sin validar let content = File.ReadAllBytes(filePath)
let safeReadReceipt baseDir fileName = let fullPath = Path.GetFullPath(Path.Combine(baseDir, fileName)) if not (fullPath.StartsWith(Path.GetFullPath(baseDir) + Path.DirectorySeparatorChar.ToString())) then Error "Ruta de fichero no permitida" else Ok (File.ReadAllBytes(fullPath))
Siempre aplicar Path.GetFullPath y verificar que el resultado tenga como prefijo el directorio base canonicalizado. Retornar Result<byte[], string> en lugar de lanzar excepción.
La API key del proveedor de pagos externo (Stripe) está hardcoded como literal de cadena en el módulo de servicio. Cualquier persona con acceso al repositorio puede obtenerla, incluyendo colaboradores externos o bots de GitHub.
let stripeApiKey = "sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
let getStripeKey (config: IConfiguration) = config.["Stripe:ApiKey"] |> Option.ofObj |> Result.ofOption "Stripe:ApiKey no configurada"
Usar IConfiguration inyectado + Azure Key Vault o variables de entorno. Rotar inmediatamente la clave comprometida. Añadir pre-commit hook con gitleaks para prevención automática.
El handler Giraffe usa .Result para esperar la tarea asíncrona de guardado de transacción,
bloqueando el hilo del ThreadPool y pudiendo causar deadlock bajo carga.
let result = saveTransactionAsync db tx .Result // bloqueo síncrono
let processPaymentHandler: HttpHandler = fun next ctx -> task { let! body = ctx.BindJsonAsync<PaymentRequest>() let! result = saveTransactionAsync db body.ToTransaction() return! json result next ctx }
Usar task { let! … } de Giraffe para mantener el flujo completamente asíncrono.
transactionId, merchantId y customerId son simples string,
lo que permite pasarlos en el orden incorrecto sin que el compilador lo detecte.
En una pasarela de pagos, confundir IDs tiene consecuencias graves.
let processPayment (txId: string) (merchantId: string) (customerId: string) = ...
type TransactionId = TransactionId of string type MerchantId = MerchantId of string type CustomerId = CustomerId of string let processPayment (txId: TransactionId) (merchantId: MerchantId) (customerId: CustomerId) = ...
Single-case DUs (Discriminated Unions) hacen que confundir IDs sea un error de compilación, sin coste en runtime.
El bloque de captura de error del procesamiento de pago captura con with _ -> None,
haciendo que cualquier fallo (timeout de red, error de base de datos) sea silencioso
e indistinguible de un pago correctamente completado.
let chargeCard amount cardToken = try stripeClient.Charge(amount, cardToken) |> Some with _ -> None // ⚠ excepción completamente silenciada
let chargeCard amount cardToken = try stripeClient.Charge(amount, cardToken) |> Ok with | :? StripeException as ex -> Error (StripeError ex.Message) | :? TimeoutException -> Error NetworkTimeout | ex -> Error (Unexpected ex.Message)
Retornar Result<ChargeResponse, PaymentError> con variantes de error explícitas. El caller debe manejar cada caso.
La transformación de DTO a entidad de dominio usa un for con acumulador mutable
en lugar de las funciones de colección estándar de F#, añadiendo complejidad innecesaria.
let mutable results = [] for dto in dtos do results <- results @ [mapToTransaction dto] // O(n²) y mutable
let results = dtos |> List.map mapToTransaction
Una sola línea, inmutable, y O(n) en lugar de O(n²) por la concatenación con @.
La secuencia de validaciones se define como Seq.map perezoso y se itera dos veces
(primero para detectar errores, luego para contar). Cada iteración re-evalúa las validaciones.
Usar |> List.ofSeq para materializar una sola vez.
Materializar con Seq.toList antes del primer uso cuando la secuencia se consume más de una vez.
Los tipos de dominio transactionStatus, paymentMethod están declarados en
camelCase. En F#, tipos, módulos y casos de DU usan PascalCase.
Renombrar a TransactionStatus, PaymentMethod. Añadir [<RequireQualifiedAccess>] al módulo para evitar colisiones de nombres.
El handler processPaymentHandler tiene 58 líneas mezclando validación, llamada a servicio,
mapeo de respuesta y logging. Difícil de testear unitariamente.
Extraer validateRequest, buildResponse y logPaymentEvent como funciones separadas. El handler queda en 10–15 líneas de pipeline con |>.
[<CLIMutable>] y anotaciones de validación de ASP.NET.FeeCalculator.fs).| # | Prioridad | Categoría | Título | Archivo |
|---|---|---|---|---|
| 1 | CRITICAL | Seguridad | Inyección SQL por interpolación | TransactionService.fs:38 |
| 2 | CRITICAL | Seguridad | Path traversal en descarga de recibos | PaymentHandlers.fs:112 |
| 3 | CRITICAL | Seguridad | Clave de API hardcoded | TransactionService.fs:14 |
| 4 | HIGH | Async | Bloqueo con .Result — riesgo deadlock | PaymentHandlers.fs:67 |
| 5 | HIGH | Tipos | Primitive obsession — IDs sin tipo | TransactionService.fs:22 |
| 6 | HIGH | Error handling | Excepción silenciada con with _ -> None |
TransactionService.fs:88 |
| 7 | HIGH | Idiomática | Bucle imperativo mutable O(n²) | TransactionService.fs:134 |
| 8 | MEDIUM | Rendimiento | Seq perezoso iterado dos veces | TransactionService.fs:162 |
| 9 | MEDIUM | Estilo | Tipos en camelCase | TransactionService.fs:18 |
| 10 | MEDIUM | Calidad | Función de 58 líneas | PaymentHandlers.fs:45 |