# Prevence command injection v PHP

> Command injection v PHP: nepoužívat shell, proc_open s polem, správné použití escapeshellarg a jak v kódu najít nebezpečná volání. S ukázkami kódu.

Zdroj: https://phpsos.de/cs/blog/command-injection-php/  
Stav: 25.10.2026  
Poskytovatel: PHP SOS, Nouzová pomoc pro PHP a bezpečnost serverů. Okamžitá pomoc přes WhatsApp: https://wa.me/491736494914?text=Dobr%C3%BD%20den%2C%20PHP%20SOS%2C%20pot%C5%99ebujeme%20pomoc%20s%20na%C5%A1%C3%ADm%20PHP%20serverem.%20Firma%3A%20%E2%80%A6%20Web%3A%20%E2%80%A6

**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
<?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](https://phpsos.de/cs/ratgeber/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
<?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:

```bash
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](https://phpsos.de/cs/blog/rechte-des-webserver-benutzers/).

> **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](https://phpsos.de/cs/blog/webserver-startet-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](https://phpsos.de/cs/leistungen/sicherheitsaudit-penetrationstest/).


## Č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

- [OWASP OS Command Injection Defense Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html)
- [Příručka PHP: proc_open](https://www.php.net/manual/de/function.proc-open.php)
- [Příručka PHP: escapeshellarg](https://www.php.net/manual/de/function.escapeshellarg.php)
