PHP и системные вызовы: практическое руководство по exec, system, passthru
Обзор системных команд в PHP
PHP предоставляет несколько функций для выполнения команд операционной системы. Наиболее распространённая из них - system(). Она отправляет команду в shell и сразу выводит результат в стандартный поток вывода. Однако в зависимости от задачи могут потребоваться другие функции: exec(), passthru() или shell_exec(). В этой статье рассматриваются все варианты с примерами кода и пояснениями.
Основное решение: system()
Как выполнить команду и увидеть её вывод непосредственно в браузере или консоли?
$output = system('ls -la');
echo "Код возврата: $output";Функция system() выполняет команду, передаёт её интерпретатору shell и сразу печатает весь вывод. Возвращает последнюю строку вывода (или false при ошибке). Вторым параметром можно получить код возврата.
Типичные ошибки:
- Команда не найдена или нет прав на выполнение - возвращается false, код возврата будет 127 (command not found).
- Вывод может быть очень большим и заблокировать скрипт. Для длительных команд лучше использовать exec() с буферизацией.
- Неэкранированные аргументы приводят к уязвимостям (command injection). Рекомендуется использовать escapeshellcmd() или escapeshellarg().
Для экранирования всей команды применяют escapeshellcmd(), для отдельного аргумента - escapeshellarg():
$file = escapeshellarg($user_file);
system("cat $file");Вариант 2: exec()
Как выполнить команду и получить её вывод в массив для дальнейшей обработки?
$output = [];
$returnCode = 0;
exec('ls -la', $output, $returnCode);
print_r($output);
echo "Код возврата: $returnCode";exec() не выводит результат автоматически. Вторым параметром передаётся массив, куда построчно записывается вывод. Код возврата - третий параметр. Подходит для захвата вывода без прямого эха.
Проблемы:
- При большом объёме вывода массив может переполнить память. В таких случаях используют passthru() или временный файл.
- Символы конца строки не обрезаются, их нужно чистить вручную.
Вариант 3: passthru()
Как выполнить команду и передать бинарные данные (например, вывод изображения) напрямую в поток вывода без буферизации?
passthru('cat image.png');passthru() работает аналогично system(), но не возвращает вывод в PHP - он напрямую отправляется в stdout. Идеально для бинарных данных (изображений, архивов), так как не изменяет содержимое.
Ошибки:
- Если не настроен заголовок Content-Type, браузер может некорректно отобразить бинарные данные.
- При ошибках команды может быть выведен текст ошибки в stdout, что нарушит поток данных.
Вариант 4: shell_exec()
Как получить весь вывод команды в виде одной строки?
$result = shell_exec('ls -la');
echo $result;
shell_exec() выполняет команду через shell и возвращает весь вывод в виде строки (без последнего символа новой строки). Если вывод пуст или ошибка, возвращает null.
Особенности:
- Аналогичен обратным кавычкам в PHP:
$result = `ls -la`; - Важно: при ошибке возвращается null, а не false. Следует проверять с помощью строгого сравнения.
Вариант 5: обратные кавычки (backticks)
Как синтаксически коротко выполнить команду?
$output = `ls -la`;
echo $output;Это эквивалент shell_exec(). Удобно для быстрых однострочных команд, но злоупотребление снижает читаемость кода.
Предостережения:
- Внутри кавычек нельзя использовать сложные переменные без конкатенации.
- Не отличается по безопасности от shell_exec() - требуется экранирование.
Вариант 6: proc_open() - продвинутый контроль
Как получить полный контроль над процессом: ввод-вывод, дескрипторы, асинхронность?
$descriptorspec = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("pipe", "w")
);
$process = proc_open('cat', $descriptorspec, $pipes);
if (is_resource($process)) {
fwrite($pipes[0], "Привет, мир!");
fclose($pipes[0]);
echo stream_get_contents($pipes[1]);
fclose($pipes[1]);
proc_close($process);
}proc_open() позволяет открыть процесс с гибким управлением потоками. Используется для интерактивных команд или когда нужно подавать данные на stdin.
Сложности:
- Требуется аккуратное закрытие всех дескрипторов и освобождение ресурса.
- Блокировки при чтении/записи могут привести к взаимоблокировке (deadlock).
Безопасность при выполнении системных команд
Все функции выполнения команд несут риск инъекций. Никогда не передавайте непроверенные пользовательские данные напрямую в команду. Всегда используйте escapeshellarg() для аргументов и escapeshellcmd() для всей команды, если это необходимо.
$filename = $_GET['file'];
$safe = escapeshellarg($filename);
system("cat $safe");
// или с помощью exec
exec("cat $safe", $out, $code);Также можно использовать whitelist разрешённых команд.
Расширенные примеры использования системных команд
Пример 1. Интерактивный запуск программы с передачей данных через proc_open.
$descriptors = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w']
];
$process = proc_open('grep "PHP"', $descriptors, $pipes);
if (is_resource($process)) {
fwrite($pipes[0], "Python\nPHP\nJavaScript\n");
fclose($pipes[0]);
$result = stream_get_contents($pipes[1]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
echo "Результат grep:\n$result";
} else {
echo "Не удалось запустить процесс.";
}Результат grep: PHP
Пример 2. Выполнение команды с ограничением времени выполнения (timeout).
$cmd = 'sleep 10 && echo done';
// Используем exec с временным файлом и сигналом через proc_open
$timeout = 3;
$descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
$process = proc_open($cmd, $descriptorspec, $pipes);
if (is_resource($process)) {
$start = time();
while (true) {
$status = proc_get_status($process);
if (!$status['running']) break;
if (time() - $start > $timeout) {
proc_terminate($process, 9);
echo "Процесс превысил время выполнения и был завершён.";
break;
}
usleep(100000);
}
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
}Процесс превысил время выполнения и был завершён.
Пример 3. Использование shell_exec для получения полного вывода команды ping с одним запросом.
$host = 'google.com';
$cmd = sprintf('ping -c 1 %s 2>&1', escapeshellarg($host));
$output = shell_exec($cmd);
if ($output === null) {
echo "Ошибка выполнения ping.";
} else {
echo "$output
";
}PING google.com (142.250.185.78): 56 data bytes 64 bytes from 142.250.185.78: icmp_seq=0 ttl=119 time=10.2 ms --- google.com ping statistics --- 1 packets transmitted, 1 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 10.2/10.2/10.2/0.0 ms
Пример 4. Передача переменных окружения в подпроцесс.
$env = ['APP_ENV' => 'production', 'SECRET' => 'abc123'];
$cmd = 'printenv | grep APP_';
$descriptors = [1 => ['pipe', 'w']];
$process = proc_open($cmd, $descriptors, $pipes, null, $env);
if (is_resource($process)) {
echo stream_get_contents($pipes[1]);
fclose($pipes[1]);
proc_close($process);
}APP_ENV=production
Пример 5. Буферизация вывода длительной команды с помощью popen и построчного чтения.
$handle = popen('tail -f /var/log/syslog', 'r');
if ($handle) {
while (!feof($handle)) {
$line = fgets($handle);
if ($line !== false) {
echo htmlspecialchars($line) . "
\n";
ob_flush();
flush();
}
}
pclose($handle);
}Этот пример предназначен для работы в консольном скрипте с отключенным буферизированием вывода PHP (ob_implicit_flush). В веб-сервере такая техника требует специальной настройки (выключение буферизации).
Пример 6. Экранирование аргументов для избежания инъекции.
$user_input = "'; rm -rf /; echo 'hacked";
$safe_arg = escapeshellarg($user_input);
$command = "echo $safe_arg";
echo "Команда: $command\n";
system($command);Команда: echo ''"'"'; rm -rf /; echo ''"'"'hacked' '\'\'; rm -rf /; echo \'hacked'
Экранирование превращает специальные символы в безопасные строки. В результате команда просто выводит введённую строку, без реального выполнения rm.