Back

Prevención de Ataques de Path Traversal en Node.js

Prevención de Ataques de Path Traversal en Node.js

Si tu aplicación Express sirve archivos basándose en la entrada del usuario—una ruta de descarga, una vista previa de carga, un recurso dinámico—tienes una potencial vulnerabilidad de path traversal. Es uno de los errores de seguridad de archivos más comunes y dañinos en Node.js, y es sorprendentemente fácil introducirlo sin darse cuenta.

Este artículo explica cómo funcionan los ataques de path traversal, por qué las “soluciones” comunes son insuficientes, y cómo se ve realmente en la práctica el manejo seguro de archivos en Node.js.

Puntos Clave

  • Los ataques de path traversal explotan la validación faltante o incompleta de rutas de archivo proporcionadas por el usuario, permitiendo a los atacantes acceder a archivos fuera de los directorios previstos.
  • path.normalize() y path.join() no son herramientas de seguridad—limpian rutas pero no previenen el directory traversal.
  • La defensa correcta es resolver rutas con path.resolve() y verificar que el resultado permanezca dentro de un directorio base permitido usando una verificación startsWith que incluya path.sep.
  • El enfoque más seguro es evitar completamente las rutas proporcionadas por el usuario mapeando la entrada a archivos mediante una búsqueda basada en ID.

¿Qué es un Ataque de Path Traversal?

Un ataque de path traversal (también llamado directory traversal) ocurre cuando un atacante manipula una entrada de ruta de archivo para acceder a archivos fuera del directorio previsto. El ejemplo clásico:

GET /download?file=../../etc/passwd

Si tu servidor ingenuamente une esa entrada con un directorio base y lee el archivo, acabas de entregar el archivo de contraseñas de tu sistema.

El ataque no es sofisticado. Explota la validación de entrada faltante o incompleta—específicamente, el fallo al verificar que una ruta resuelta permanezca dentro de un directorio permitido.

Por Qué path.normalize() y path.join() No Son Suficientes

Esto es lo más importante que debes entender sobre la prevención de path traversal en Node.js: estas utilidades no son herramientas de seguridad.

path.normalize() resuelve secuencias .. y limpia barras redundantes. path.join() concatena segmentos. Ninguna de las dos previene el traversal—solo producen una ruta más limpia que aún podría apuntar fuera de tu directorio previsto.

Considera este patrón común pero inseguro:

// ❌ Inseguro: path.join no previene el traversal
app.get('/download', (req, res) => {
  const filePath = path.join(__dirname, 'uploads', req.query.file)
  res.sendFile(filePath)
})

Si req.query.file es ../../etc/passwd, path.join() lo resuelve limpiamente—y envía el archivo incorrecto.

La codificación URL lo empeora. Un atacante podría enviar ..%2F..%2Fetc%2Fpasswd. Express decodifica automáticamente los parámetros URL, por lo que debes validar el valor decodificado que recibes. Si estás manejando URLs crudas tú mismo, decodifica una vez (con manejo de errores apropiado) antes de la validación.

El Patrón Correcto: Resolver y Verificar la Contención

El enfoque confiable para el manejo seguro de archivos en Node.js es:

  1. Resolver la ruta proporcionada por el usuario contra tu directorio base usando path.resolve().
  2. Verificar que la ruta resuelta permanezca dentro de ese directorio.
const path = require('path')

const BASE_DIR = path.resolve(__dirname, 'uploads')

function safeResolve(userInput) {
  const resolved = path.resolve(BASE_DIR, userInput)

  // Asegurar que la ruta resuelta esté dentro de BASE_DIR
  if (!resolved.startsWith(BASE_DIR + path.sep) && resolved !== BASE_DIR) {
    return null
  }

  return resolved
}

// ✅ Ruta segura de descarga de archivos en Express
app.get('/download', (req, res) => {
  const safePath = safeResolve(req.query.file)

  if (!safePath) {
    return res.status(403).send('Acceso denegado')
  }

  res.sendFile(safePath)
})

Nota la adición de path.sep. Sin ella, un directorio base de /uploads permitiría incorrectamente que /uploads-other/secret.txt pase la verificación de prefijo.

En Windows, path.resolve() y path.sep manejan correctamente las barras invertidas, por lo que este patrón funciona en todas las plataformas. Para una validación más estricta, también puedes comparar rutas usando path.relative() y rechazar cualquier resultado que escape del directorio base.

Ten en cuenta que los enlaces simbólicos aún pueden apuntar fuera de tu directorio base. Si estás sirviendo archivos sensibles, resuelve las rutas reales con fs.realpath() antes de enviarlos.

Mejor Aún: Evita Completamente las Rutas Proporcionadas por el Usuario

El enfoque más seguro es no usar rutas proporcionadas por el usuario en absoluto. En su lugar, mapea la entrada del usuario a archivos indirectamente:

// ✅ Búsqueda de archivos basada en ID — sin construcción de rutas desde entrada del usuario
const FILES = {
  'report-2024': 'reports/annual-2024.pdf',
  'invoice-001': 'invoices/inv-001.pdf',
}

app.get('/download/:id', (req, res) => {
  const filePath = FILES[req.params.id]

  if (!filePath) {
    return res.status(404).send('No encontrado')
  }

  res.sendFile(path.resolve(__dirname, 'secure', filePath))
})

Esto elimina completamente el riesgo de traversal. Si tu caso de uso lo permite—descargas de archivos, exportaciones de documentos, recursos específicos del usuario—este es el patrón que debes elegir primero.

Una Nota sobre el Servicio Estático de Express

express.static() es generalmente más seguro para servir recursos públicos porque restringe el acceso a un directorio raíz definido. El riesgo aparece cuando escribes rutas personalizadas de servicio de archivos que construyen rutas a partir de parámetros de solicitud o exponen directorios no previstos. La configuración y la selección de la raíz aún importan.

Conclusión

El path traversal es una vulnerabilidad directa con una solución directa: nunca confíes en la entrada del usuario como una ruta de archivo. Resuélvela contra una base fija, verifica la contención con path.resolve() y una verificación startsWith que incluya path.sep, y valida la entrada decodificada antes de usarla. Cuando sea posible, prefiere búsquedas basadas en ID que eviten la construcción de rutas a partir de la entrada del usuario por completo. path.normalize() y path.join() son utilidades útiles—simplemente no son garantías de seguridad.

Preguntas Frecuentes

Ayuda al servir archivos desde un directorio raíz definido y manejar la resolución de rutas internamente, pero no es una garantía de seguridad completa. La mala configuración o la exposición de directorios sensibles aún pueden crear riesgo. Las rutas personalizadas de servicio de archivos que usan entrada del usuario son donde ocurren la mayoría de las vulnerabilidades.

No. Filtrar o rechazar cadenas que contengan secuencias de punto-punto es frágil. Los atacantes pueden eludir tales verificaciones con codificación URL, doble codificación o separadores de ruta específicos de la plataforma. La defensa confiable es resolver la ruta completa con path.resolve() y luego verificar que el resultado caiga dentro de tu directorio base previsto usando una verificación startsWith o path.relative().

Sí. Usar path.resolve() y path.sep hace que la verificación de contención funcione en todas las plataformas. path.resolve() normaliza las rutas de Windows, y path.sep se evalúa al separador correcto. Para una validación más estricta, usa path.relative() para asegurar que la ruta resuelta no escape del directorio base.

Sí. Después de confirmar que la ruta resuelta está dentro de tu directorio base, verifica que el archivo existe usando fs.existsSync() o fs.access() antes de llamar a res.sendFile(). Esto previene filtrar información sobre la estructura de tu directorio a través de mensajes de error y te da control sobre la respuesta cuando falta un archivo.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay