Накратко: Command Injection в PHP предотвратявате най-сигурно, като изобщо не извиквате shell: много задачи PHP функциите или библиотеките вършат директно. Където е нужна външна програма, предавате командата от PHP 7.4 насам като масив на proc_open(), тогава PHP стартира програмата без shell. Стойностите проверявате спрямо списък с разрешени стойности, приключвате опциите с -- и пускате процеса с минимални права.
Защо едно системно извикване има такава власт
Обработка на изображения, създаване на PDF, архивиране, антивирусна проверка, запитване за DNS запис: за такива задачи PHP приложенията с готовност извикват външни програми. Технически това е удобно, достатъчен е един ред с exec() или shell_exec(). От гледна точка на сигурността това е едно от най-опасните места в кода.
Когато една команда се сглобява като текст и се изпълнява от shell, shell решава кое е команда и кое са данни. Ако отвън попадне непроверена стойност, тя може да промени смисъла на командата. Последицата в най-лошия случай е същата като при PHP2Shell: изпълнение на произволни команди на сървъра с правата на PHP процеса.
Разликата: PHP2Shell отстранявате с актуализация на PHP. Command Injection в собствения код остава след всяка актуализация. Освен това такива места често се крият в помощни скриптове, предназначени само за администратори, например за поддръжка, диагностика или импорти, и по-късно остават достъпни без защита.
Къде възниква Command Injection в PHP
Опасно е всяко място, където PHP стартира shell или програма. Функциите за това са:
| Функция | Ползва ли shell? | Забележка |
|---|---|---|
exec(), system(), passthru(), shell_exec() |
да | Команда като текст, shell я интерпретира |
| Оператор backtick | да | съответства на shell_exec() |
popen() |
да | Команда като текст |
proc_open() с низ |
да | Команда като текст |
proc_open() с масив |
не | От PHP 7.4, аргументите се предават директно |
mail() с пети параметър |
непряко | Допълнителни параметри към пощенската програма |
Типично несигурният образец изглежда така. Името на файла идва от заявката и попада директно в командния ред:
<?php
// НЕСИГУРНО: стойност от заявката става част от shell команда
$datei = $_POST['datei'];
exec('convert uploads/' . $datei . ' -resize 800x800 vorschau/' . $datei);
Първи избор: изобщо без shell
Най-ефективната защита е да замените системното извикване. OWASP го посочва изрично като първа линия на защита. За много задачи в PHP има собствени функции или разширения:
- Директории и файлове:
mkdir(),copy(),rename(),unlink() - Архиви:
ZipArchiveилиPharData - Изображения: GD или Imagick като PHP разширение
- Мрежови заявки:
dns_get_record(),gethostbyname(), cURL - Контролни суми:
hash_file()
Така въпросът кои знаци интерпретира shell изчезва напълно. Освен това функциите за системни команди могат след това да се блокират в конфигурацията, както описва наръчникът Правилно използване на disable_functions.
Когато е нужна външна програма: proc_open с масив
От PHP 7.4 proc_open() приема командата като масив. Според наръчника на PHP процесът тогава се стартира директно, без заобикаляне през shell, а PHP поема необходимото екраниране на аргументите. Знаци като точка и запетая или pipe вече нямат специално значение.
<?php
declare(strict_types=1);
$name = basename((string) ($_POST['datei'] ?? ''));
if (!preg_match('/^[a-z0-9_-]{1,64}\.(jpg|png)$/', $name)) {
throw new InvalidArgumentException('Ungültiger Dateiname.');
}
$befehl = ['/usr/bin/convert', '/srv/app/uploads/' . $name, '-resize', '800x800',
'/srv/app/vorschau/' . $name];
$prozess = proc_open($befehl, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
if (!is_resource($prozess)) {
throw new RuntimeException('Programm konnte nicht gestartet werden.');
}
$fehlerausgabe = stream_get_contents($pipes[2]);
$status = proc_close($prozess);
Три неща правят примера устойчив: програмата е зададена в кода с пълен път, името на файла се проверява спрямо тесен шаблон и всеки променлив аргумент започва с абсолютен път, така че не може да бъде прочетен като опция. Където една стойност стои сама, приключете опциите с --, доколкото програмата го поддържа според документацията, или отхвърляйте стойности, които започват с тире.
При задачи, които редовно изискват външни програми, например обработка на изображения или PDF в по-голям обем, си струва разделяне: уеб приложението само поставя задача в опашка, а отделна фонова услуга със собствен потребител и собствени права я изпълнява. PHP процесът, който отговаря на заявки от интернет, тогава вече не се нуждае от функции за системни команди и те могат да се блокират за него. Външните програми трябва освен това да имат ограничения за време и памет, за да не блокира една-единствена задача сървъра.
Особен случай е mail(). Петият параметър предава допълнителни опции на пощенската програма на системата. Стойности от формуляри, например адрес на подател, никога не бива да попадат там непроверени. По-устойчива е пощенска библиотека, която изпраща по SMTP и не изисква системно извикване.
escapeshellarg: как да го използвате правилно и да знаете границите му
Където съществуващият код още работи с командни редове, escapeshellarg() за всяка отделна стойност е минималната мярка. Функцията поставя стойността в кавички, така че shell да я третира като точно един аргумент. escapeshellcmd() е неподходяща за входни данни от потребители, защото допуска допълнителни параметри, както показва OWASP с пример.
Важни ограничения на escapeshellarg():
- Остава възможна Argument Injection: Екранирана стойност, която започва с тире, целевата програма все пак чете като опция.
- Windows се държи по друг начин: Там функцията според наръчника заменя определени знаци като
%и"с интервали; стойностите стигат променени. - Набор от знаци: В зависимост от настройката за Locale на сървъра знаци извън ASCII могат да се загубят.
Как да откриете местата в съществуващия код
Търсенето на системни извиквания е бързо и дава обозрим списък:
grep -rnE "\b(exec|system|passthru|shell_exec|popen|proc_open)\s*\(" src/ --include="*.php"
grep -rn '`' src/ --include="*.php" | grep -v '^\s*//'
За всяко намерено място трябва да има отговор на три въпроса: може ли извикването да се замени с PHP функции? Влизат ли в него стойности отвън? С какви права работи процесът? Последният въпрос е решаващ за щетите в най-лошия случай. PHP процес със собствен потребител и без права за запис върху кода върши по-малко поразии, вижте Least Privilege: права на потребителя на уеб сървъра.
Ако процес на PHP-FPM или на уеб сървъра стартира shell, това е силен предупредителен сигнал за пробив. Как да го разпознаете, описва Предупредителен сигнал: уеб сървърът стартира shell.
Как помага PHP SOS конкретно?
- Търсим всички системни извиквания във Вашето приложение, в добавките и в помощните скриптове.
- Където е възможно, заменяме извикванията с PHP функции, а останалите преминаваме към извиквания без shell.
- Блокираме ненужните функции в конфигурацията на PHP и проверяваме правата на PHP потребителя.
- Тестваме промените и документираме кои програми приложението още има право да извиква.
Проверка на собствен софтуер предлагаме като одит на сигурността и тест за проникване.
Често задавани въпроси
Достатъчно ли е escapeshellarg срещу Command Injection?
То предотвратява една стойност да бъде изпълнена като друга команда. Не предотвратява стойността да бъде прочетена като опция на извиканата програма, ако започва с тире. Затова допълнително проверявайте стойностите и, където програмата го поддържа, приключвайте опциите с --.
Каква е разликата между escapeshellarg и escapeshellcmd?
escapeshellarg третира отделна стойност като точно един аргумент. escapeshellcmd екранира само метазнаците в цял команден ред и допуска допълнителни параметри. За входни данни от потребители escapeshellarg е правилният избор, а извикване без shell е още по-добро.
Трябва ли да блокирам exec и system чрез disable_functions?
Ако Вашето приложение не ги ползва, да. Това ограничава щетите след пробив. То обаче не замества сигурния код, защото приложенията, които се нуждаят от команди, продължават да се нуждаят от тези функции.
Как да разбера дали е била използвана Command Injection?
Силен предупредителен сигнал са PHP процеси или процеси на уеб сървъра, които стартират shell като sh или bash. Към това се добавят непознати процеси, нови файлове в папките за качване и необичайни изходящи връзки.