Revisión de Código F#

CultivaPay API — Módulo de Transacciones
PR #47 · feature/payment-transaction-module
BLOQUEADO
3 CRITICAL · 4 HIGH
requiere correcciones
3
Críticos
4
Altos
3
Medios
6
Superados

Archivos Revisados

src/CultivaPay.Domain/TransactionService.fs
src/CultivaPay.Api/PaymentHandlers.fs
Críticos — Seguridad
CRITICAL Inyección SQL por interpolación de cadenas TransactionService.fs:38

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.

F# ❌ Código actual
let getTransactionsByMerchant (db: DbContext) (merchantId: string) =
    let sql = $"SELECT * FROM transactions WHERE merchant_id = '{merchantId}'"
    db.Database.ExecuteSqlRaw(sql)  // ⚠ SQL injection directo
F# ✅ Corrección
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.

CRITICAL Path traversal en descarga de recibos PaymentHandlers.fs:112

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.

F# ❌ Código actual
let baseDir = "/var/cultivapay/receipts"
let filePath = Path.Combine(baseDir, fileName) // sin validar
let content = File.ReadAllBytes(filePath)
F# ✅ Corrección
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.

CRITICAL Clave de API hardcoded en código fuente TransactionService.fs:14

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.

F#❌ Código actual
let stripeApiKey = "sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
F#✅ Corrección
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.

Altos — Idiomática Funcional y Tipos
HIGH Bloqueo de async con .Result — riesgo de deadlock PaymentHandlers.fs:67

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.

F#❌ Código actual
let result = saveTransactionAsync db tx .Result  // bloqueo síncrono
F#✅ Corrección
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.

HIGH Primitive obsession — IDs sin tipo de dominio TransactionService.fs:22–55

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.

F#❌ Código actual
let processPayment (txId: string) (merchantId: string) (customerId: string) = ...
F#✅ Corrección
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.

HIGH Excepción silenciada — fallo de pago invisible TransactionService.fs:88

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.

F#❌ Código actual
let chargeCard amount cardToken =
    try stripeClient.Charge(amount, cardToken) |> Some
    with _ -> None  // ⚠ excepción completamente silenciada
F#✅ Corrección
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.

HIGH Bucle imperativo donde List.map es la expresión idiomática TransactionService.fs:134

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.

F#❌ Código actual
let mutable results = []
for dto in dtos do
    results <- results @ [mapToTransaction dto]  // O(n²) y mutable
F#✅ Corrección
let results = dtos |> List.map mapToTransaction
✔

Una sola línea, inmutable, y O(n) en lugar de O(n²) por la concatenación con @.

Medios — Calidad y Rendimiento
MEDIUM Seq perezoso en hot path de validación de pagos TransactionService.fs:162

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.

MEDIUM Nombres en camelCase para tipos (violación de convención) TransactionService.fs:18–20

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.

MEDIUM Función de 58 líneas — demasiado larga PaymentHandlers.fs:45–103

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 |>.

✅ Aspectos Correctos (6)

✓ Autenticación JWT en todos los endpoints — middleware configurado correctamente en Program.fs.
✓ DTOs de entrada decorados con [<CLIMutable>] y anotaciones de validación de ASP.NET.
✓ Tests unitarios presentes para el módulo de cálculo de comisiones (FeeCalculator.fs).
✓ Migrations de EF Core en carpeta separada, sin cambios destructivos en este PR.
✓ Logging estructurado con Serilog en todas las rutas de éxito.
✓ Ausencia de código comentado o TODOs pendientes de resolución urgente.

Resumen de Hallazgos

# 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