Express
Tutorial Express: Paths y directorios
Aprende a manejar rutas seguras y crear directorios en Express 5 con técnicas de validación, sanitización y manejo de errores para aplicaciones robustas.
Aprende Express y certifícateManejo de rutas seguras
El manejo seguro de rutas en Express 5 es fundamental para prevenir vulnerabilidades como path traversal y acceso no autorizado a archivos del sistema. Cuando trabajamos con rutas de archivos proporcionadas por usuarios, debemos implementar validaciones y sanitización para garantizar que solo se acceda a recursos permitidos.
Express 5 proporciona herramientas integradas y mejores prácticas que nos permiten manejar rutas de forma segura, especialmente cuando servimos archivos estáticos o procesamos uploads de usuarios.
Validación de rutas con path.resolve()
La función path.resolve()
de Node.js es esencial para normalizar rutas y prevenir ataques de path traversal. Esta función convierte rutas relativas en absolutas y elimina secuencias peligrosas como ../
:
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
app.get('/files/:filename', (req, res) => {
const filename = req.params.filename;
// Resolver la ruta de forma segura
const safePath = path.resolve(__dirname, 'uploads', filename);
const uploadsDir = path.resolve(__dirname, 'uploads');
// Verificar que la ruta esté dentro del directorio permitido
if (!safePath.startsWith(uploadsDir)) {
return res.status(403).json({ error: 'Acceso denegado' });
}
res.sendFile(safePath);
});
Sanitización de nombres de archivo
Los nombres de archivo proporcionados por usuarios pueden contener caracteres peligrosos o secuencias de escape. Es crucial sanitizar estos nombres antes de usarlos:
import path from 'path';
function sanitizeFilename(filename) {
// Remover caracteres peligrosos
const sanitized = filename
.replace(/[^a-zA-Z0-9.-]/g, '_') // Solo permitir caracteres seguros
.replace(/\.{2,}/g, '.') // Evitar múltiples puntos consecutivos
.replace(/^\.+|\.+$/g, ''); // Remover puntos al inicio y final
// Limitar longitud
return sanitized.substring(0, 255);
}
app.post('/upload', (req, res) => {
const originalName = req.body.filename;
const safeName = sanitizeFilename(originalName);
if (!safeName) {
return res.status(400).json({ error: 'Nombre de archivo inválido' });
}
const safePath = path.join(__dirname, 'uploads', safeName);
// Procesar archivo...
});
Implementación de middleware de seguridad
Un middleware personalizado puede centralizar la lógica de validación de rutas y aplicarla consistentemente en toda la aplicación:
function securePathMiddleware(baseDir) {
const resolvedBaseDir = path.resolve(baseDir);
return (req, res, next) => {
const requestedPath = req.params.path || req.query.path;
if (!requestedPath) {
return res.status(400).json({ error: 'Ruta requerida' });
}
try {
const resolvedPath = path.resolve(resolvedBaseDir, requestedPath);
// Verificar que la ruta esté dentro del directorio base
if (!resolvedPath.startsWith(resolvedBaseDir + path.sep)) {
return res.status(403).json({ error: 'Ruta no permitida' });
}
// Añadir la ruta segura al objeto request
req.safePath = resolvedPath;
next();
} catch (error) {
res.status(400).json({ error: 'Ruta inválida' });
}
};
}
// Uso del middleware
app.use('/secure-files/*', securePathMiddleware('./public'));
app.get('/secure-files/:path(*)', (req, res) => {
// req.safePath ya está validada y es segura
res.sendFile(req.safePath);
});
Validación de extensiones de archivo
Restringir las extensiones de archivo permitidas añade una capa adicional de seguridad, especialmente importante cuando se manejan uploads:
const ALLOWED_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.pdf', '.txt'];
function validateFileExtension(filename) {
const ext = path.extname(filename).toLowerCase();
return ALLOWED_EXTENSIONS.includes(ext);
}
app.get('/download/:filename', (req, res) => {
const filename = req.params.filename;
// Validar extensión
if (!validateFileExtension(filename)) {
return res.status(400).json({ error: 'Tipo de archivo no permitido' });
}
const safePath = path.resolve(__dirname, 'downloads', filename);
const downloadsDir = path.resolve(__dirname, 'downloads');
if (!safePath.startsWith(downloadsDir)) {
return res.status(403).json({ error: 'Acceso denegado' });
}
res.download(safePath);
});
Manejo de rutas con express.static()
Express 5 mejora la seguridad del middleware express.static()
con opciones adicionales para controlar el acceso a archivos:
app.use('/public', express.static('public', {
dotfiles: 'deny', // Denegar acceso a archivos ocultos
index: false, // No servir archivos index automáticamente
redirect: false, // No redirigir rutas que terminan en /
setHeaders: (res, path) => {
// Configurar headers de seguridad
res.set('X-Content-Type-Options', 'nosniff');
res.set('X-Frame-Options', 'DENY');
}
}));
Implementación de límites de profundidad
Para prevenir el acceso a directorios anidados excesivamente profundos, podemos implementar límites de profundidad:
function validatePathDepth(requestPath, maxDepth = 3) {
const pathParts = requestPath.split(path.sep).filter(part => part && part !== '.');
return pathParts.length <= maxDepth;
}
app.get('/files/:path(*)', (req, res) => {
const requestPath = req.params.path;
if (!validatePathDepth(requestPath)) {
return res.status(400).json({ error: 'Ruta demasiado profunda' });
}
// Continuar con validación de ruta segura...
const safePath = path.resolve(__dirname, 'files', requestPath);
// Resto de la lógica...
});
Estas técnicas de manejo seguro de rutas son esenciales para mantener la integridad y seguridad de aplicaciones Express 5, especialmente cuando se manejan archivos proporcionados por usuarios o se sirve contenido dinámico basado en parámetros de entrada.
Creación de directorios
La creación de directorios en Express 5 es una operación fundamental cuando desarrollamos aplicaciones que manejan archivos, uploads de usuarios o necesitan organizar contenido dinámicamente. Express 5 aprovecha las capacidades nativas de Node.js para crear directorios de forma eficiente y segura, proporcionando herramientas que nos permiten manejar la estructura de carpetas de manera robusta.
Cuando trabajamos con aplicaciones Express que requieren almacenamiento de archivos, es común necesitar crear directorios para organizar uploads, logs, cachés temporales o contenido generado dinámicamente.
Creación básica con fs.mkdir()
El módulo fs
de Node.js proporciona métodos síncronos y asíncronos para crear directorios. En Express 5, utilizamos principalmente la versión asíncrona para evitar bloquear el event loop:
import fs from 'fs/promises';
import path from 'path';
app.post('/create-user-folder', async (req, res) => {
const { userId } = req.body;
if (!userId) {
return res.status(400).json({ error: 'ID de usuario requerido' });
}
try {
const userDir = path.join(__dirname, 'uploads', userId);
await fs.mkdir(userDir);
res.json({
message: 'Directorio creado exitosamente',
path: userDir
});
} catch (error) {
if (error.code === 'EEXIST') {
return res.status(409).json({ error: 'El directorio ya existe' });
}
res.status(500).json({ error: 'Error al crear directorio' });
}
});
Creación recursiva de directorios
La opción recursive: true
permite crear directorios anidados en una sola operación, creando automáticamente los directorios padre que no existan:
app.post('/setup-project-structure', async (req, res) => {
const { projectName } = req.body;
try {
const projectPath = path.join(__dirname, 'projects', projectName);
// Crear estructura completa de directorios
const directories = [
path.join(projectPath, 'assets', 'images'),
path.join(projectPath, 'assets', 'documents'),
path.join(projectPath, 'temp'),
path.join(projectPath, 'exports')
];
// Crear todos los directorios de forma concurrente
await Promise.all(
directories.map(dir => fs.mkdir(dir, { recursive: true }))
);
res.json({
message: 'Estructura de proyecto creada',
directories: directories
});
} catch (error) {
res.status(500).json({ error: 'Error al crear estructura' });
}
});
Middleware para creación automática de directorios
Un middleware personalizado puede verificar y crear directorios automáticamente antes de procesar requests que los requieran:
function ensureDirectoryExists(basePath) {
return async (req, res, next) => {
try {
const targetDir = path.join(basePath, req.params.category || '');
// Verificar si el directorio existe
try {
await fs.access(targetDir);
} catch {
// Si no existe, crearlo
await fs.mkdir(targetDir, { recursive: true });
console.log(`Directorio creado: ${targetDir}`);
}
req.targetDirectory = targetDir;
next();
} catch (error) {
res.status(500).json({ error: 'Error al preparar directorio' });
}
};
}
// Uso del middleware
app.use('/upload/:category', ensureDirectoryExists('./uploads'));
app.post('/upload/:category', (req, res) => {
// req.targetDirectory ya está disponible y garantizado que existe
const uploadPath = path.join(req.targetDirectory, req.file.originalname);
// Procesar upload...
});
Creación con permisos específicos
En sistemas Unix/Linux, podemos especificar permisos de directorio durante la creación para controlar el acceso:
app.post('/create-secure-folder', async (req, res) => {
const { folderName, isPublic } = req.body;
try {
const folderPath = path.join(__dirname, 'data', folderName);
// Definir permisos según el tipo de carpeta
const mode = isPublic ? 0o755 : 0o700; // 755 para público, 700 para privado
await fs.mkdir(folderPath, {
recursive: true,
mode: mode
});
res.json({
message: 'Carpeta creada con permisos específicos',
path: folderPath,
permissions: mode.toString(8)
});
} catch (error) {
res.status(500).json({ error: 'Error al crear carpeta segura' });
}
});
Validación antes de crear directorios
Implementar validaciones robustas antes de crear directorios previene errores y mejora la experiencia del usuario:
async function validateDirectoryCreation(basePath, dirName) {
// Validar nombre del directorio
if (!/^[a-zA-Z0-9_-]+$/.test(dirName)) {
throw new Error('Nombre de directorio contiene caracteres inválidos');
}
// Validar longitud
if (dirName.length > 50) {
throw new Error('Nombre de directorio demasiado largo');
}
const fullPath = path.join(basePath, dirName);
// Verificar que no exceda límites de profundidad
const relativePath = path.relative(basePath, fullPath);
if (relativePath.split(path.sep).length > 5) {
throw new Error('Estructura de directorios demasiado profunda');
}
return fullPath;
}
app.post('/create-validated-directory', async (req, res) => {
const { directoryName } = req.body;
try {
const validatedPath = await validateDirectoryCreation(
path.join(__dirname, 'user-content'),
directoryName
);
await fs.mkdir(validatedPath, { recursive: true });
res.json({
message: 'Directorio creado y validado',
path: validatedPath
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
Creación de directorios temporales
Para directorios temporales que se crean y eliminan dinámicamente, podemos implementar un sistema de gestión automática:
import { randomUUID } from 'crypto';
class TempDirectoryManager {
constructor(basePath) {
this.basePath = basePath;
this.tempDirs = new Map();
}
async createTempDirectory(ttl = 3600000) { // TTL por defecto: 1 hora
const tempId = randomUUID();
const tempPath = path.join(this.basePath, 'temp', tempId);
await fs.mkdir(tempPath, { recursive: true });
// Programar limpieza automática
const cleanupTimer = setTimeout(async () => {
await this.cleanupTempDirectory(tempId);
}, ttl);
this.tempDirs.set(tempId, {
path: tempPath,
timer: cleanupTimer,
created: Date.now()
});
return { id: tempId, path: tempPath };
}
async cleanupTempDirectory(tempId) {
const tempInfo = this.tempDirs.get(tempId);
if (tempInfo) {
clearTimeout(tempInfo.timer);
try {
await fs.rm(tempInfo.path, { recursive: true, force: true });
} catch (error) {
console.error(`Error limpiando directorio temporal: ${error.message}`);
}
this.tempDirs.delete(tempId);
}
}
}
const tempManager = new TempDirectoryManager(__dirname);
app.post('/create-temp-workspace', async (req, res) => {
try {
const { id, path: tempPath } = await tempManager.createTempDirectory();
res.json({
message: 'Espacio temporal creado',
workspaceId: id,
path: tempPath,
expiresIn: '1 hora'
});
} catch (error) {
res.status(500).json({ error: 'Error al crear espacio temporal' });
}
});
Manejo de errores específicos
Diferentes códigos de error requieren respuestas específicas para proporcionar feedback útil al cliente:
app.post('/create-directory-with-error-handling', async (req, res) => {
const { path: requestedPath } = req.body;
try {
const fullPath = path.join(__dirname, 'managed-dirs', requestedPath);
await fs.mkdir(fullPath, { recursive: true });
res.json({
message: 'Directorio creado exitosamente',
path: fullPath
});
} catch (error) {
let statusCode = 500;
let message = 'Error interno del servidor';
switch (error.code) {
case 'EEXIST':
statusCode = 409;
message = 'El directorio ya existe';
break;
case 'EACCES':
statusCode = 403;
message = 'Permisos insuficientes para crear directorio';
break;
case 'ENOSPC':
statusCode = 507;
message = 'Espacio insuficiente en disco';
break;
case 'ENAMETOOLONG':
statusCode = 400;
message = 'Nombre de directorio demasiado largo';
break;
}
res.status(statusCode).json({
error: message,
code: error.code
});
}
});
La creación de directorios en Express 5 requiere considerar aspectos de seguridad, validación y manejo de errores para construir aplicaciones robustas que gestionen eficientemente la estructura de archivos y directorios.
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 Paths y directorios con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.