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ícateValidació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.
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.
Introducción A Expressjs
Introducción Y Entorno
Instalación De Express
Introducción Y Entorno
Estados Http
Routing
Métodos Delete
Routing
Parámetros Y Query Strings
Routing
Métodos Get
Routing
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.