From 8f0b9546d464d8d731a0675889d634ac1f416d92 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Fri, 30 Jan 2026 15:23:50 -0300 Subject: [PATCH] =?UTF-8?q?Fix:=20Seguridad=20de=20Im=C3=A1genes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/ImageStorageService.cs | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/Backend/MotoresArgentinosV2.Infrastructure/Services/ImageStorageService.cs b/Backend/MotoresArgentinosV2.Infrastructure/Services/ImageStorageService.cs index da9d728..8ce4a0c 100644 --- a/Backend/MotoresArgentinosV2.Infrastructure/Services/ImageStorageService.cs +++ b/Backend/MotoresArgentinosV2.Infrastructure/Services/ImageStorageService.cs @@ -78,11 +78,27 @@ public class ImageStorageService : IImageStorageService { string hex = BitConverter.ToString(headerBytes.Take(8).ToArray()); _logger.LogWarning("Firma de archivo inválida para {Extension}: {HexBytes}", ext, hex); - throw new Exception($"El archivo parece corrupto o tiene una firma inválida ({hex}). El sistema acepta JPG, PNG y WEBP reales."); + throw new Exception($"El archivo parece corrupto o tiene una firma inválida ({hex})."); } } + try { + // 4. PREVENCIÓN DoS: Validar dimensiones sin cargar la imagen completa en memoria + // Esto evita que una imagen de 1KB que dice ser de 50000x50000px cuelgue el servidor + using (var stream = file.OpenReadStream()) + { + var info = await Image.IdentifyAsync(stream); + if (info == null) throw new Exception("No se pudo identificar el formato de la imagen."); + + const int MaxDimension = 5000; // 5000px es más que suficiente para un aviso + if (info.Width > MaxDimension || info.Height > MaxDimension) + { + _logger.LogWarning("Intento de subir imagen con dimensiones excesivas: {W}x{H}", info.Width, info.Height); + throw new Exception($"Las dimensiones de la imagen ({info.Width}x{info.Height}) exceden el límite de seguridad de {MaxDimension}px."); + } + } + // 1. Definir rutas var uploadFolder = Path.Combine(_env.WebRootPath, "uploads", "ads", adId.ToString()); if (!Directory.Exists(uploadFolder)) Directory.CreateDirectory(uploadFolder); @@ -94,7 +110,7 @@ public class ImageStorageService : IImageStorageService var filePath = Path.Combine(uploadFolder, fileName); var thumbPath = Path.Combine(uploadFolder, thumbName); - // 2. Cargar y Procesar con ImageSharp + // 2. Cargar y Procesar con ImageSharp (Aquí ya es seguro porque validamos dimensiones e identidad) using (var image = await Image.LoadAsync(file.OpenReadStream())) { // A. Guardar imagen principal (Optimized: Max width 1280px) @@ -102,22 +118,21 @@ public class ImageStorageService : IImageStorageService { image.Mutate(x => x.Resize(new ResizeOptions { - Size = new Size(1280, 0), // 0 mantiene aspect ratio + Size = new Size(1280, 0), Mode = ResizeMode.Max })); } await image.SaveAsJpegAsync(filePath); - // B. Generar Thumbnail (Max width 400px para grillas) + // B. Generar Thumbnail image.Mutate(x => x.Resize(new ResizeOptions { Size = new Size(400, 300), - Mode = ResizeMode.Crop // Recorte inteligente para que queden parejitas + Mode = ResizeMode.Crop })); await image.SaveAsJpegAsync(thumbPath); } - // Retornar ruta relativa web de la imagen principal return $"/uploads/ads/{adId}/{fileName}"; } catch (Exception ex)