Express

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ícate

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

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 Paths y directorios con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.