Express

Express

Tutorial Express: Validación de archivos

Aprende a validar tipos y controlar tamaños de archivos en Express 5 para mejorar la seguridad y eficiencia de tus aplicaciones web.

Aprende Express y certifícate

Validación de tipos

La validación de tipos de archivo es fundamental para mantener la seguridad y funcionalidad de nuestras aplicaciones Express. Esta validación nos permite controlar qué tipos de archivos pueden subir los usuarios, evitando potenciales vulnerabilidades de seguridad y garantizando que solo se procesen archivos del formato esperado.

Express 5 nos proporciona múltiples enfoques para implementar esta validación, desde verificaciones básicas del MIME type hasta análisis más sofisticados del contenido real del archivo. La estrategia más robusta combina ambas técnicas para crear una barrera de seguridad efectiva.

Validación mediante MIME type

El MIME type es la forma más directa de identificar el tipo de archivo. Cuando utilizamos middleware como multer, podemos acceder a esta información a través de la propiedad mimetype del archivo:

const multer = require('multer');

const storage = multer.diskStorage({
  destination: './uploads/',
  filename: (req, file, cb) => {
    cb(null, Date.now() + '-' + file.originalname);
  }
});

const fileFilter = (req, file, cb) => {
  // Tipos MIME permitidos
  const allowedTypes = [
    'image/jpeg',
    'image/png',
    'image/gif',
    'application/pdf',
    'text/plain'
  ];
  
  if (allowedTypes.includes(file.mimetype)) {
    cb(null, true); // Archivo aceptado
  } else {
    cb(new Error(`Tipo de archivo no permitido: ${file.mimetype}`), false);
  }
};

const upload = multer({ 
  storage: storage,
  fileFilter: fileFilter
});

Esta implementación básica nos permite filtrar archivos según su MIME type declarado. Sin embargo, es importante recordar que esta información puede ser manipulada por usuarios malintencionados, por lo que no debemos confiar únicamente en ella.

Validación por extensión de archivo

Complementar la validación de MIME type con la verificación de extensiones añade una capa adicional de seguridad:

const path = require('path');

const validateFileExtension = (filename, allowedExtensions) => {
  const extension = path.extname(filename).toLowerCase();
  return allowedExtensions.includes(extension);
};

const fileFilter = (req, file, cb) => {
  const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.pdf', '.txt'];
  const allowedMimeTypes = [
    'image/jpeg',
    'image/png', 
    'image/gif',
    'application/pdf',
    'text/plain'
  ];
  
  // Validación combinada
  const validExtension = validateFileExtension(file.originalname, allowedExtensions);
  const validMimeType = allowedMimeTypes.includes(file.mimetype);
  
  if (validExtension && validMimeType) {
    cb(null, true);
  } else {
    cb(new Error('Tipo de archivo no válido'), false);
  }
};

Validación avanzada mediante magic numbers

Para una seguridad más robusta, podemos implementar validación basada en los magic numbers o firmas de archivo. Estos son bytes específicos al inicio de cada archivo que identifican su tipo real:

const fs = require('fs');

const getFileSignature = (filePath) => {
  const buffer = fs.readFileSync(filePath);
  return buffer.subarray(0, 8).toString('hex').toUpperCase();
};

const validateFileSignature = (filePath) => {
  const signature = getFileSignature(filePath);
  
  // Firmas conocidas de tipos de archivo
  const signatures = {
    'FFD8FF': 'image/jpeg',
    '89504E47': 'image/png',
    '47494638': 'image/gif',
    '25504446': 'application/pdf',
    '504B0304': 'application/zip'
  };
  
  // Verificar si la firma coincide con algún tipo permitido
  for (const [sig, mimeType] of Object.entries(signatures)) {
    if (signature.startsWith(sig)) {
      return mimeType;
    }
  }
  
  return null; // Tipo no reconocido
};

Implementación completa en rutas Express

Integrando todas estas técnicas en una ruta Express completa:

app.post('/upload', upload.single('file'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'No se ha subido ningún archivo' });
  }
  
  try {
    // Validación adicional post-upload
    const detectedType = validateFileSignature(req.file.path);
    
    if (!detectedType) {
      // Eliminar archivo no válido
      fs.unlinkSync(req.file.path);
      return res.status(400).json({ error: 'Tipo de archivo no reconocido' });
    }
    
    // Verificar que el tipo detectado coincida con el declarado
    if (detectedType !== req.file.mimetype) {
      fs.unlinkSync(req.file.path);
      return res.status(400).json({ 
        error: 'El tipo de archivo no coincide con su contenido real' 
      });
    }
    
    res.json({
      message: 'Archivo subido correctamente',
      file: {
        name: req.file.filename,
        type: detectedType,
        size: req.file.size
      }
    });
    
  } catch (error) {
    // Limpiar archivo en caso de error
    if (req.file && fs.existsSync(req.file.path)) {
      fs.unlinkSync(req.file.path);
    }
    
    res.status(500).json({ error: 'Error procesando el archivo' });
  }
});

Manejo de errores específicos

Express 5 nos permite crear middleware de manejo de errores específico para validación de archivos:

const handleFileValidationError = (err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    switch (err.code) {
      case 'LIMIT_FILE_SIZE':
        return res.status(400).json({ error: 'Archivo demasiado grande' });
      case 'LIMIT_UNEXPECTED_FILE':
        return res.status(400).json({ error: 'Campo de archivo inesperado' });
      default:
        return res.status(400).json({ error: 'Error de validación de archivo' });
    }
  }
  
  if (err.message.includes('Tipo de archivo')) {
    return res.status(400).json({ error: err.message });
  }
  
  next(err);
};

// Aplicar el middleware de manejo de errores
app.use(handleFileValidationError);

Esta aproximación multicapa nos proporciona una validación robusta que combina verificaciones rápidas con análisis más profundos del contenido, asegurando que solo los archivos legítimos y seguros sean procesados por nuestra aplicación Express.

Límites de tamaño

El control del tamaño de archivos es esencial para proteger nuestros servidores Express de ataques de denegación de servicio y garantizar un uso eficiente de los recursos del sistema. Sin límites apropiados, los usuarios podrían subir archivos extremadamente grandes que consuman todo el espacio disponible o la memoria del servidor.

Express 5, junto con middleware como multer, nos ofrece múltiples niveles de control para gestionar el tamaño de los archivos de forma granular y eficiente.

Configuración básica de límites

La forma más directa de establecer límites de tamaño es a través de la configuración del middleware de upload:

const multer = require('multer');

const upload = multer({
  dest: './uploads/',
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB en bytes
    files: 3, // Máximo 3 archivos por request
    fields: 10, // Máximo 10 campos de formulario
    fieldSize: 1024 * 1024 // 1MB por campo de texto
  }
});

Estos límites actúan como una primera barrera de protección, rechazando automáticamente cualquier archivo que exceda los parámetros establecidos antes de que se complete la subida.

Límites diferenciados por tipo de archivo

En aplicaciones reales, diferentes tipos de archivo requieren límites específicos. Por ejemplo, las imágenes pueden tener un límite menor que los documentos PDF:

const createUploadMiddleware = (fileType) => {
  const limits = {
    image: { fileSize: 2 * 1024 * 1024 }, // 2MB para imágenes
    document: { fileSize: 10 * 1024 * 1024 }, // 10MB para documentos
    video: { fileSize: 50 * 1024 * 1024 }, // 50MB para videos
    default: { fileSize: 1 * 1024 * 1024 } // 1MB por defecto
  };

  return multer({
    dest: './uploads/',
    limits: limits[fileType] || limits.default,
    fileFilter: (req, file, cb) => {
      // Validación de tipo según el contexto
      const typeValidation = {
        image: ['image/jpeg', 'image/png', 'image/gif'],
        document: ['application/pdf', 'application/msword'],
        video: ['video/mp4', 'video/avi']
      };

      const allowedTypes = typeValidation[fileType] || [];
      
      if (allowedTypes.includes(file.mimetype)) {
        cb(null, true);
      } else {
        cb(new Error(`Tipo no permitido para categoría ${fileType}`), false);
      }
    }
  });
};

// Uso en rutas específicas
app.post('/upload/image', createUploadMiddleware('image').single('photo'), handleImageUpload);
app.post('/upload/document', createUploadMiddleware('document').single('file'), handleDocumentUpload);

Validación progresiva durante la subida

Para archivos muy grandes, podemos implementar validación en tiempo real que monitoree el progreso de la subida y la detenga si es necesario:

const progressiveUpload = multer({
  storage: multer.diskStorage({
    destination: './uploads/',
    filename: (req, file, cb) => {
      cb(null, `${Date.now()}-${file.originalname}`);
    }
  }),
  limits: {
    fileSize: 20 * 1024 * 1024 // 20MB límite base
  }
});

app.post('/upload/progressive', (req, res) => {
  let uploadedBytes = 0;
  const maxSize = 15 * 1024 * 1024; // 15MB límite real
  
  // Middleware personalizado para monitorear progreso
  const upload = progressiveUpload.single('file');
  
  // Interceptar el stream de datos
  req.on('data', (chunk) => {
    uploadedBytes += chunk.length;
    
    if (uploadedBytes > maxSize) {
      req.pause();
      return res.status(413).json({
        error: 'Archivo demasiado grande',
        maxSize: `${maxSize / (1024 * 1024)}MB`,
        received: `${uploadedBytes / (1024 * 1024)}MB`
      });
    }
  });
  
  upload(req, res, (err) => {
    if (err) {
      return res.status(400).json({ error: err.message });
    }
    
    res.json({
      message: 'Archivo subido correctamente',
      size: `${req.file.size / (1024 * 1024)}MB`
    });
  });
});

Límites dinámicos basados en contexto

Los límites adaptativos permiten ajustar las restricciones según el usuario, plan de suscripción o tipo de cuenta:

const getDynamicLimits = (req) => {
  const userPlan = req.user?.plan || 'free';
  
  const planLimits = {
    free: {
      fileSize: 1 * 1024 * 1024, // 1MB
      dailyQuota: 10 * 1024 * 1024, // 10MB diarios
      maxFiles: 5
    },
    premium: {
      fileSize: 25 * 1024 * 1024, // 25MB
      dailyQuota: 500 * 1024 * 1024, // 500MB diarios
      maxFiles: 50
    },
    enterprise: {
      fileSize: 100 * 1024 * 1024, // 100MB
      dailyQuota: 5 * 1024 * 1024 * 1024, // 5GB diarios
      maxFiles: 200
    }
  };
  
  return planLimits[userPlan] || planLimits.free;
};

const dynamicUpload = (req, res, next) => {
  const limits = getDynamicLimits(req);
  
  const upload = multer({
    dest: './uploads/',
    limits: {
      fileSize: limits.fileSize,
      files: limits.maxFiles
    }
  }).array('files');
  
  upload(req, res, next);
};

Validación post-upload y limpieza

Después de la subida, es importante verificar y limpiar archivos que no cumplan con criterios adicionales:

const validateAndCleanup = async (req, res, next) => {
  if (!req.files || req.files.length === 0) {
    return next();
  }
  
  const limits = getDynamicLimits(req);
  let totalSize = 0;
  const filesToRemove = [];
  
  // Calcular tamaño total y validar cada archivo
  for (const file of req.files) {
    totalSize += file.size;
    
    // Verificar límite individual
    if (file.size > limits.fileSize) {
      filesToRemove.push(file.path);
      continue;
    }
    
    // Verificar integridad del archivo
    try {
      const stats = await fs.promises.stat(file.path);
      if (stats.size !== file.size) {
        filesToRemove.push(file.path);
      }
    } catch (error) {
      filesToRemove.push(file.path);
    }
  }
  
  // Verificar cuota total
  if (totalSize > limits.dailyQuota) {
    // Marcar todos los archivos para eliminación
    req.files.forEach(file => filesToRemove.push(file.path));
  }
  
  // Limpiar archivos no válidos
  for (const filePath of filesToRemove) {
    try {
      await fs.promises.unlink(filePath);
    } catch (error) {
      console.error(`Error eliminando archivo: ${filePath}`, error);
    }
  }
  
  // Filtrar archivos válidos
  req.files = req.files.filter(file => !filesToRemove.includes(file.path));
  
  if (req.files.length === 0 && filesToRemove.length > 0) {
    return res.status(413).json({
      error: 'Ningún archivo cumple con los límites establecidos',
      limits: {
        maxFileSize: `${limits.fileSize / (1024 * 1024)}MB`,
        dailyQuota: `${limits.dailyQuota / (1024 * 1024)}MB`
      }
    });
  }
  
  next();
};

app.post('/upload/validated', dynamicUpload, validateAndCleanup, (req, res) => {
  res.json({
    message: 'Archivos procesados correctamente',
    files: req.files.map(file => ({
      name: file.filename,
      size: `${(file.size / 1024).toFixed(2)}KB`,
      type: file.mimetype
    }))
  });
});

Manejo de errores de límites

Express 5 nos permite crear respuestas específicas para diferentes tipos de violaciones de límites:

const handleSizeErrors = (err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    switch (err.code) {
      case 'LIMIT_FILE_SIZE':
        return res.status(413).json({
          error: 'Archivo demasiado grande',
          code: 'FILE_TOO_LARGE',
          maxSize: '5MB'
        });
        
      case 'LIMIT_FILE_COUNT':
        return res.status(413).json({
          error: 'Demasiados archivos',
          code: 'TOO_MANY_FILES',
          maxFiles: 10
        });
        
      case 'LIMIT_FIELD_COUNT':
        return res.status(413).json({
          error: 'Demasiados campos en el formulario',
          code: 'TOO_MANY_FIELDS'
        });
        
      case 'LIMIT_UNEXPECTED_FILE':
        return res.status(400).json({
          error: 'Campo de archivo inesperado',
          code: 'UNEXPECTED_FIELD'
        });
    }
  }
  
  next(err);
};

app.use(handleSizeErrors);

Esta implementación multicapa de control de tamaños nos proporciona flexibilidad para adaptarnos a diferentes necesidades mientras mantenemos la seguridad y estabilidad del servidor Express.

Aprende Express online

Otras lecciones de Express

Accede a todas las lecciones de Express y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Express y certifícate

Ejercicios de programación de Express

Evalúa tus conocimientos de esta lección Validación de archivos con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.