Aktuální situace: PHP2Shell. Dvě kritické zranitelnosti PHP (CVSS 9.2) umožňují spustit škodlivý kód bez přihlášení. Záplaty od 21. 10. 2026.Co nyní udělat →

Prevence command injection v PHP

Jak vývojáři v PHP předcházejí command injection: nahrazují systémové příkazy funkcemi PHP, používají proc_open bez shellu, kontrolují argumenty a hledají nebezpečná místa ve stávajícím kódu.

Stav 5 min čteníBezpečný vývoj

Stručně: Command injection v PHP nejspolehlivěji zabráníte tím, že vůbec nevoláte shell: mnoho úloh zvládnou přímo funkce PHP nebo knihovny. Kde je externí program nutný, předávejte příkaz od PHP 7.4 jako pole do proc_open(), PHP pak program spustí bez shellu. Hodnoty kontrolujte proti seznamu povolených hodnot, volby ukončete pomocí -- a proces nechte běžet s minimálními oprávněními.

Proč má systémové volání tak velkou moc

Úprava obrázků, tvorba PDF, archivace, antivirová kontrola, dotaz na záznam DNS: pro takové úlohy aplikace PHP rády volají externí programy. Technicky je to pohodlné, stačí jeden řádek s exec() nebo shell_exec(). Z hlediska bezpečnosti jde o jedno z nejnebezpečnějších míst v kódu.

Pokud se příkaz sestaví jako text a provede ho shell, rozhoduje shell o tom, co je příkaz a co data. Pokud se dovnitř dostane nezkontrolovaná hodnota zvenčí, může změnit význam příkazu. Důsledek je v nejhorším případě stejný jako u PHP2Shell: spuštění libovolných příkazů na serveru s právy procesu PHP.

Rozdíl: PHP2Shell odstraníte aktualizací PHP. Command injection ve vlastním kódu zůstává po každé aktualizaci. Navíc se taková místa často skrývají v pomocných skriptech, které byly určeny jen pro administrátory, například pro údržbu, diagnostiku nebo importy, a později zůstanou dostupná bez ochrany.

Kde command injection v PHP vzniká

Ohrožené je každé místo, kde PHP spouští shell nebo program. Funkce k tomu jsou:

Funkce Používá shell? Poznámka
exec(), system(), passthru(), shell_exec() ano příkaz jako text, shell ho interpretuje
Operátor zpětných apostrofů ano odpovídá shell_exec()
popen() ano příkaz jako text
proc_open() s řetězcem ano příkaz jako text
proc_open() s polem ne od PHP 7.4, argumenty se předávají přímo
mail() s pátým parametrem nepřímo další parametry pro poštovní program

Typický nebezpečný vzor vypadá takto. Název souboru pochází z požadavku a končí přímo v příkazovém řádku:

<?php
// NEBEZPEČNÉ: hodnota z požadavku se stává součástí příkazu shellu
$datei = $_POST['datei'];
exec('convert uploads/' . $datei . ' -resize 800x800 vorschau/' . $datei);

První volba: žádný shell

Nejúčinnější ochranou je systémové volání nahradit. OWASP to výslovně uvádí jako první linii obrany. Pro mnoho úloh existují v PHP vlastní funkce nebo rozšíření:

  • Adresáře a soubory: mkdir(), copy(), rename(), unlink()
  • Archivy: ZipArchive nebo PharData
  • Obrázky: GD nebo Imagick jako rozšíření PHP
  • Síťové dotazy: dns_get_record(), gethostbyname(), cURL
  • Kontrolní součty: hash_file()

Tím zcela zmizí otázka, které znaky shell interpretuje. Funkce pro systémové příkazy lze navíc v konfiguraci zablokovat, jak popisuje příručka Správné použití disable_functions.

Když je externí program nutný: proc_open s polem

Od PHP 7.4 přijímá proc_open() příkaz jako pole. Podle příručky PHP se proces spustí přímo, bez oklik přes shell, a PHP převezme potřebné maskování argumentů. Znaky jako středník nebo roura pak nemají žádný zvláštní význam.

<?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);

Příklad činí odolným tři věci: program je v kódu pevně zadán s plnou cestou, název souboru se kontroluje proti úzkému vzoru a každý proměnný argument začíná absolutní cestou, takže ho nelze přečíst jako volbu. Kde hodnota stojí samostatně, ukončete volby pomocí --, pokud to program podle dokumentace podporuje, nebo odmítněte hodnoty začínající pomlčkou.

U úloh, které pravidelně potřebují externí programy, například zpracování obrázků nebo PDF ve větším rozsahu, se vyplatí oddělení: webová aplikace jen vloží úkol do fronty a samostatná služba na pozadí s vlastním uživatelem a vlastními právy ho provede. Proces PHP, který odpovídá na požadavky z internetu, pak funkce pro systémové příkazy nepotřebuje a lze je pro něj zablokovat. Externí programy by měly mít navíc limity času a paměti, aby jediný úkol neblokoval server.

Zvláštním případem je mail(). Pátý parametr předává další volby poštovnímu programu systému. Hodnoty z formulářů, například adresa odesílatele, tam nikdy nesmějí skončit nezkontrolované. Robustnější je poštovní knihovna, která odesílá přes SMTP a nepotřebuje systémové volání.

escapeshellarg: správné použití a znalost mezí

Kde stávající kód ještě pracuje s příkazovými řádky, je escapeshellarg() pro každou jednotlivou hodnotu minimálním opatřením. Funkce hodnotu uzavře do uvozovek, takže ji shell zpracuje jako právě jeden argument. escapeshellcmd() se pro uživatelské vstupy nehodí, protože připouští další parametry, jak ukazuje OWASP na příkladu.

Důležité meze escapeshellarg():

  1. Argument injection zůstává možná: Maskovaná hodnota začínající pomlčkou se cílovým programem přesto čte jako volba.
  2. Windows se chová jinak: Funkce tam podle příručky nahrazuje určité znaky jako % a " mezerami; hodnoty tedy dorazí změněné.
  3. Znaková sada: V závislosti na nastavení locale serveru se mohou ztratit znaky mimo ASCII.

Nalezení problémových míst ve stávajícím kódu

Vyhledávání systémových volání je rychlé a dává přehledný seznam:

grep -rnE "\b(exec|system|passthru|shell_exec|popen|proc_open)\s*\(" src/ --include="*.php"
grep -rn '`' src/ --include="*.php" | grep -v '^\s*//'

Každý nález potřebuje odpověď na tři otázky: Lze volání nahradit funkcemi PHP? Vtékají dovnitř hodnoty zvenčí? S jakými právy proces běží? Poslední otázka je rozhodující pro škodu v krizovém případě. Proces PHP s vlastním uživatelem a bez práva zápisu do kódu způsobí méně škody, viz Least Privilege: oprávnění uživatele webserveru.

Důležité

Pokud proces PHP-FPM nebo webserveru spustí shell, je to silné varovné znamení průniku. Jak to rozpoznat, popisuje článek Varovné znamení: webserver spouští shell.

Jak konkrétně pomáhá PHP SOS?

  • Vyhledáme všechna systémová volání ve vaší aplikaci, v pluginech a v pomocných skriptech.
  • Volání, kde to jde, nahradíme funkcemi PHP a ostatní převedeme na volání bez shellu.
  • Nepotřebné funkce zablokujeme v konfiguraci PHP a zkontrolujeme práva uživatele PHP.
  • Změny otestujeme a zdokumentujeme, které programy smí aplikace ještě volat.

Kontrolu vlastního softwaru nabízíme jako bezpečnostní audit a penetrační test.

Časté dotazy

Stačí escapeshellarg proti command injection?

Brání tomu, aby se hodnota provedla jako další příkaz. Nebrání tomu, aby ji volaný program četl jako volbu, pokud začíná pomlčkou. Proto navíc kontrolujte hodnoty a tam, kde to program podporuje, ukončete volby pomocí --.

Jaký je rozdíl mezi escapeshellarg a escapeshellcmd?

escapeshellarg zachází s jednou hodnotou jako s právě jedním argumentem. escapeshellcmd maskuje jen metaznaky v celém příkazovém řádku a připouští další parametry. Pro uživatelské vstupy je správnou volbou escapeshellarg, ještě lepší je volání bez shellu.

Mám exec a system zablokovat přes disable_functions?

Pokud je vaše aplikace nepotřebuje, ano. Omezí to škodu po průniku. Nenahradí to ale bezpečný kód, protože aplikace, které příkazy potřebují, tyto funkce dál potřebují.

Jak poznám, že byla command injection zneužita?

Silným varovným znamením jsou procesy PHP nebo webserveru, které spouštějí shell jako sh nebo bash. K tomu přistupují neznámé procesy, nové soubory ve složkách pro uploady a neobvyklá odchozí spojení.

Zdroje

Okamžitý kontakt

Každá hodina se počítá. Napište nám hned.

Stručně popište systém, verzi PHP a pozorování. Ozveme se, vyjasníme situaci a budeme pracovat na dálku, dokud nebude incident uzavřen a zdokumentován.

Okamžitá pomoc