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 как имя файла в хранилище, оригинальное имя хранить только в БД
Связанные темы
- _MOC Node.js
- Node.js -- безопасность (Helmet, rate-limit)
- Статические файлы
- Загрузка файлов -- Multer