Path Traversal

Path Traversal (Directory Traversal, ../ атака) — уязвимость, при которой злоумышленник использует последовательность ../ в параметрах запроса для доступа к файлам за пределами разрешённой директории сервера.

Зачем нужно

Уязвимость позволяет читать произвольные файлы системы: .env с секретами, /etc/passwd, конфигурационные файлы. Это одна из топовых уязвимостей в OWASP Top 10 (A05 — Security Misconfiguration). Node.js-приложения особенно уязвимы при ручной работе с fs и пользовательским вводом без санитизации.

Где используется

Знание необходимо при:

  • Раздаче файлов по именам из URL (GET /files/:filename)
  • Загрузке и скачивании пользовательских файлов
  • Функциях просмотра логов или документов по имени

Основной контент

Пример уязвимого кода

// УЯЗВИМО — прямое использование пользовательского ввода в пути
app.get('/files/:filename', (req, res) => {
  const filePath = path.join(__dirname, 'uploads', req.params.filename);
  res.sendFile(filePath);
});

// Атака: GET /files/../../.env
// filePath = /app/uploads/../../.env = /app/.env
// Сервер отдаёт .env с секретами!

Исправление через path.basename

const path = require('path');

// БЕЗОПАСНО — path.basename удаляет все ../
app.get('/files/:filename', (req, res) => {
  // '../../../.env' → '.env'
  // Но '.env' всё ещё опасен если есть в uploads/
  const filename = path.basename(req.params.filename);
  const filePath = path.join(__dirname, 'uploads', filename);

  res.sendFile(filePath);
});

Исправление через проверку prefix

app.get('/files/:filename', (req, res) => {
  const filename = path.basename(req.params.filename);
  const uploadsDir = path.resolve(__dirname, 'uploads');
  const filePath = path.resolve(uploadsDir, filename);

  // Проверить что итоговый путь начинается с разрешённой директории
  if (!filePath.startsWith(uploadsDir + path.sep)) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  res.sendFile(filePath);
});

Полная защита с аутентификацией

const path = require('path');
const fs = require('fs').promises;

app.get('/downloads/:fileId', authMiddleware, async (req, res) => {
  // 1. Искать файл по ID в БД, а не по имени
  const file = await FileRepository.findById(req.params.fileId);
  if (!file) return res.status(404).json({ error: 'Not found' });

  // 2. Проверить права
  if (file.userId !== req.user.id) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  // 3. Путь формируется из доверенных данных БД, не из запроса
  const filePath = path.join(__dirname, 'uploads', file.storedName);

  // 4. Дополнительная проверка
  const uploadsDir = path.resolve(__dirname, 'uploads');
  if (!path.resolve(filePath).startsWith(uploadsDir)) {
    return res.status(400).json({ error: 'Invalid file path' });
  }

  res.download(filePath, file.originalName);
});

express.static уже защищён

// express.static автоматически предотвращает Path Traversal
// GET /../../../.env → 403 Forbidden
app.use('/files', express.static(path.join(__dirname, 'uploads')));
// Но это не даёт контроля над правами доступа

Тест уязвимости

# Попытка Path Traversal
curl http://localhost:3000/files/../../.env
curl http://localhost:3000/files/%2e%2e%2f%2e%2e%2f.env  # URL-encoded
curl http://localhost:3000/files/....//....//....// .env  # другие паттерны

Частые ошибки

  • Использовать req.params.filename напрямую в path.join — всегда применять path.basename или lookup по ID
  • path.basename как единственная защитаbasename('../secret.txt')'secret.txt', но если в uploads/ есть .env — уязвимость; нужна проверка через startsWith
  • URL-декодирование%2e%2e%2f это ../; Node.js декодирует автоматически, но стоит знать о вариантах кодирования
  • Хранить файлы по предсказуемым именам — использовать UUID/hash как имя файла в хранилище, оригинальное имя хранить только в БД

Связанные темы

Ресурсы