Предотвращение атак с использованием обхода путей в Node.js
Если ваше Express-приложение обслуживает файлы на основе пользовательского ввода — маршрут загрузки, предварительный просмотр загруженного файла, динамический ресурс — у вас есть потенциальная уязвимость обхода путей. Это одна из наиболее распространённых и опасных ошибок безопасности при работе с файлами в Node.js, и её удивительно легко допустить, даже не осознавая этого.
В этой статье объясняется, как работают атаки с обходом путей, почему распространённые «исправления» недостаточны и как на практике выглядит безопасная работа с файлами в Node.js.
Ключевые выводы
- Атаки с обходом путей эксплуатируют отсутствие или неполную валидацию пользовательских файловых путей, позволяя атакующим получать доступ к файлам за пределами предназначенных директорий.
path.normalize()иpath.join()не являются инструментами безопасности — они очищают пути, но не предотвращают обход директорий.- Правильная защита заключается в разрешении путей с помощью
path.resolve()и проверке того, что результат остаётся в пределах разрешённой базовой директории, используя проверкуstartsWithс добавлениемpath.sep. - Наиболее безопасный подход — полностью избегать пользовательских путей, сопоставляя ввод с файлами через поиск на основе идентификаторов.
Что такое атака с обходом путей?
Атака с обходом путей (также называемая обходом директорий) происходит, когда атакующий манипулирует вводом файлового пути для доступа к файлам за пределами предназначенной директории. Классический пример:
GET /download?file=../../etc/passwd
Если ваш сервер наивно объединяет этот ввод с базовой директорией и читает файл, вы только что передали файл паролей вашей системы.
Атака не является сложной. Она эксплуатирует отсутствие или неполную валидацию ввода — в частности, неспособность проверить, что разрешённый путь остаётся в пределах разрешённой директории.
Почему path.normalize() и path.join() недостаточно
Это самое важное, что нужно понять о предотвращении обхода путей в Node.js: эти утилиты не являются инструментами безопасности.
path.normalize() разрешает последовательности .. и очищает избыточные слеши. path.join() объединяет сегменты. Ни один из них не предотвращает обход — они просто создают более чистый путь, который всё ещё может указывать за пределы вашей предназначенной директории.
Рассмотрим этот распространённый, но небезопасный паттерн:
// ❌ Небезопасно: path.join не предотвращает обход
app.get('/download', (req, res) => {
const filePath = path.join(__dirname, 'uploads', req.query.file)
res.sendFile(filePath)
})
Если req.query.file равен ../../etc/passwd, path.join() разрешает его корректно — и отправляет неправильный файл.
URL-кодирование усугубляет ситуацию. Атакующий может отправить ..%2F..%2Fetc%2Fpasswd. Express автоматически декодирует URL-параметры, поэтому вы должны валидировать полученное декодированное значение. Если вы обрабатываете сырые URL самостоятельно, декодируйте один раз (с правильной обработкой ошибок) перед валидацией.
Правильный паттерн: разрешение и проверка вхождения
Надёжный подход к безопасной работе с файлами в Node.js:
- Разрешите пользовательский путь относительно вашей базовой директории, используя
path.resolve(). - Проверьте, что разрешённый путь остаётся в пределах этой директории.
const path = require('path')
const BASE_DIR = path.resolve(__dirname, 'uploads')
function safeResolve(userInput) {
const resolved = path.resolve(BASE_DIR, userInput)
// Убедитесь, что разрешённый путь находится внутри BASE_DIR
if (!resolved.startsWith(BASE_DIR + path.sep) && resolved !== BASE_DIR) {
return null
}
return resolved
}
// ✅ Безопасный маршрут загрузки файлов Express
app.get('/download', (req, res) => {
const safePath = safeResolve(req.query.file)
if (!safePath) {
return res.status(403).send('Access denied')
}
res.sendFile(safePath)
})
Обратите внимание на добавление path.sep. Без него базовая директория /uploads ошибочно разрешила бы прохождение проверки префикса для /uploads-other/secret.txt.
В Windows path.resolve() и path.sep корректно обрабатывают обратные слеши, поэтому этот паттерн работает на разных платформах. Для более строгой валидации вы также можете сравнивать пути, используя path.relative(), и отклонять любой результат, выходящий за пределы базовой директории.
Имейте в виду, что символические ссылки всё ещё могут указывать за пределы вашей базовой директории. Если вы обслуживаете конфиденциальные файлы, разрешайте реальные пути с помощью fs.realpath() перед их отправкой.
Discover how at OpenReplay.com.
Ещё лучше: полностью избегайте пользовательских путей
Наиболее безопасный подход — вообще не использовать пользовательские пути. Вместо этого сопоставляйте пользовательский ввод с файлами косвенно:
// ✅ Поиск файлов на основе ID — нет построения пути из пользовательского ввода
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('Not found')
}
res.sendFile(path.resolve(__dirname, 'secure', filePath))
})
Это полностью устраняет риск обхода. Если ваш случай использования позволяет это — загрузка файлов, экспорт документов, пользовательские ресурсы — это паттерн, к которому следует обращаться в первую очередь.
Замечание о статической раздаче в Express
express.static() обычно безопаснее для раздачи публичных ресурсов, поскольку ограничивает доступ к определённой корневой директории. Риск появляется, когда вы пишете пользовательские маршруты раздачи файлов, которые строят пути из параметров запроса или открывают непредусмотренные директории. Конфигурация и выбор корня всё ещё имеют значение.
Заключение
Обход путей — это простая уязвимость с простым исправлением: никогда не доверяйте пользовательскому вводу как файловому пути. Разрешайте его относительно фиксированной базы, проверяйте вхождение с помощью path.resolve() и проверки startsWith с добавлением path.sep, и валидируйте декодированный ввод перед использованием. Когда возможно, предпочитайте поиск на основе идентификаторов, который полностью обходит построение пути из пользовательского ввода. path.normalize() и path.join() — полезные утилиты, но не гарантии безопасности.
Часто задаваемые вопросы
Он помогает, раздавая файлы из определённой корневой директории и обрабатывая разрешение путей внутренне, но это не гарантия полной безопасности. Неправильная конфигурация или открытие конфиденциальных директорий всё ещё могут создать риск. Большинство уязвимостей возникает в пользовательских маршрутах раздачи файлов, использующих пользовательский ввод.
Нет. Фильтрация или отклонение строк, содержащих последовательности точка-точка, является хрупкой. Атакующие могут обойти такие проверки с помощью URL-кодирования, двойного кодирования или специфичных для платформы разделителей путей. Надёжная защита заключается в разрешении полного пути с помощью path.resolve() и последующей проверке того, что результат попадает в вашу предназначенную базовую директорию, используя проверку startsWith или path.relative().
Да. Использование path.resolve() и path.sep делает проверку вхождения работающей на разных платформах. path.resolve() нормализует пути Windows, а path.sep оценивается как правильный разделитель. Для более строгой валидации используйте path.relative(), чтобы убедиться, что разрешённый путь не выходит за пределы базовой директории.
Да. После подтверждения того, что разрешённый путь находится в вашей базовой директории, проверьте существование файла, используя fs.existsSync() или fs.access() перед вызовом res.sendFile(). Это предотвращает утечку информации о структуре вашей директории через сообщения об ошибках и даёт вам контроль над ответом, когда файл отсутствует.
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.