# Zapobieganie command injection w PHP

> Zapobieganie command injection w PHP: unikanie powłoki, proc_open z tablicą, prawidłowe użycie escapeshellarg i wyszukiwanie niebezpiecznych wywołań w kodzie.

Źródło: https://phpsos.de/pl/blog/command-injection-php/  
Stan: 25.10.2026  
Dostawca: PHP SOS, Pomoc awaryjna PHP i bezpieczeństwo serwerów. Natychmiastowa pomoc przez WhatsApp: https://wa.me/491736494914?text=Dzie%C5%84%20dobry%2C%20PHP%20SOS%2C%20potrzebujemy%20pomocy%20z%20naszym%20serwerem%20PHP.%20Firma%3A%20%E2%80%A6%20Strona%20internetowa%3A%20%E2%80%A6

**Krótko:** Command injection w PHP zapobiegasz najpewniej, w ogóle nie wywołując powłoki: wiele zadań wykonują bezpośrednio funkcje PHP lub biblioteki. Tam, gdzie potrzebny jest zewnętrzny program, przekazujesz polecenie od PHP 7.4 jako tablicę do `proc_open()`, wtedy PHP uruchamia program bez powłoki. Wartości sprawdzasz względem listy dozwolonych, opcje kończysz za pomocą `--`, a proces uruchamiasz z minimalnymi uprawnieniami.

## Dlaczego wywołanie systemowe daje tak dużą władzę

Obróbka obrazów, generowanie PDF, archiwizowanie, skanowanie antywirusowe, zapytanie o wpis DNS: do takich zadań aplikacje PHP chętnie wywołują zewnętrzne programy. Technicznie jest to wygodne, wystarczy jedna linia z `exec()` lub `shell_exec()`. Pod względem bezpieczeństwa to jedno z najniebezpieczniejszych miejsc w kodzie.

Jeśli polecenie jest składane jako tekst i wykonywane przez powłokę, to powłoka decyduje, co jest poleceniem, a co danymi. Jeśli wartość z zewnątrz trafi tam bez kontroli, może zmienić znaczenie polecenia. W najgorszym przypadku skutek jest taki sam jak przy PHP2Shell: wykonanie dowolnych poleceń na serwerze z uprawnieniami procesu PHP.

Różnica: PHP2Shell usuwasz aktualizacją PHP. Command injection we własnym kodzie pozostaje po każdej aktualizacji. Dochodzi do tego, że takie miejsca często tkwią w skryptach pomocniczych przeznaczonych tylko dla administratorów, na przykład do konserwacji, diagnostyki lub importów, a później pozostają dostępne bez ochrony.

## Gdzie powstaje command injection w PHP

Zagrożone jest każde miejsce, w którym PHP uruchamia powłokę lub program. Funkcje do tego:

| Funkcja | Używa powłoki? | Wskazówka |
|---|---|---|
| `exec()`, `system()`, `passthru()`, `shell_exec()` | tak | Polecenie jako tekst, powłoka go interpretuje |
| Operator odwrotnego apostrofu (backtick) | tak | odpowiada `shell_exec()` |
| `popen()` | tak | Polecenie jako tekst |
| `proc_open()` z ciągiem | tak | Polecenie jako tekst |
| `proc_open()` z tablicą | nie | od PHP 7.4, argumenty są przekazywane bezpośrednio |
| `mail()` z piątym parametrem | pośrednio | dodatkowe parametry dla programu pocztowego |

Typowy niebezpieczny wzorzec wygląda tak. Nazwa pliku pochodzi z żądania i trafia bezpośrednio do wiersza poleceń:

```php
<?php
// NIEBEZPIECZNE: wartość z żądania staje się częścią polecenia powłoki
$datei = $_POST['datei'];
exec('convert uploads/' . $datei . ' -resize 800x800 vorschau/' . $datei);
```

## Pierwszy wybór: w ogóle bez powłoki

Najskuteczniejszą ochroną jest zastąpienie wywołania systemowego. OWASP wyraźnie nazywa to pierwszą linią obrony. Do wielu zadań PHP ma własne funkcje lub rozszerzenia:

- Katalogi i pliki: `mkdir()`, `copy()`, `rename()`, `unlink()`
- Archiwa: `ZipArchive` lub `PharData`
- Obrazy: GD lub Imagick jako rozszerzenie PHP
- Zapytania sieciowe: `dns_get_record()`, `gethostbyname()`, cURL
- Sumy kontrolne: `hash_file()`

Dzięki temu pytanie, które znaki interpretuje powłoka, znika całkowicie. Ponadto funkcje do poleceń systemowych można wtedy zablokować w konfiguracji, jak opisuje poradnik [Prawidłowe użycie disable_functions](https://phpsos.de/pl/ratgeber/disable-functions/).

## Gdy potrzebny jest zewnętrzny program: proc_open z tablicą

Od PHP 7.4 `proc_open()` przyjmuje polecenie jako tablicę. Według podręcznika PHP proces jest wtedy uruchamiany bezpośrednio, bez pośrednictwa powłoki, a PHP przejmuje konieczne maskowanie argumentów. Znaki takie jak średnik czy potok nie mają wtedy już znaczenia specjalnego.

```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);
```

Trzy rzeczy czynią ten przykład odpornym: program jest na stałe wpisany w kodzie z pełną ścieżką, nazwa pliku jest sprawdzana względem wąskiego wzorca, a każdy zmienny argument zaczyna się od ścieżki bezwzględnej, więc nie może zostać odczytany jako opcja. Tam, gdzie wartość stoi samotnie, kończysz opcje za pomocą `--`, o ile program według dokumentacji to obsługuje, albo odrzucasz wartości zaczynające się od myślnika.

W przypadku zadań, które regularnie wymagają zewnętrznych programów, na przykład obróbki obrazów lub PDF w większej skali, warto rozdzielić odpowiedzialności: aplikacja WWW tylko umieszcza zlecenie w kolejce, a oddzielna usługa w tle z własnym użytkownikiem i własnymi uprawnieniami je wykonuje. Proces PHP, który odpowiada na żądania z internetu, nie potrzebuje wtedy już funkcji do poleceń systemowych i można je dla niego zablokować. Zewnętrzne programy powinny ponadto mieć limity czasu i pamięci, aby pojedyncze zlecenie nie blokowało serwera.

Szczególnym przypadkiem jest `mail()`. Piąty parametr przekazuje dodatkowe opcje do programu pocztowego systemu. Wartości z formularzy, na przykład adres nadawcy, nigdy nie mogą tam trafić bez kontroli. Bardziej odporna jest biblioteka pocztowa, która wysyła przez SMTP i nie potrzebuje wywołania systemowego.

## escapeshellarg: właściwe użycie i znajomość granic

Tam, gdzie istniejący kod nadal pracuje z wierszami poleceń, `escapeshellarg()` dla każdej pojedynczej wartości jest środkiem minimalnym. Funkcja ujmuje wartość w cudzysłów, tak aby powłoka traktowała ją jako dokładnie jeden argument. `escapeshellcmd()` nie nadaje się dla danych od użytkownika, ponieważ dopuszcza dodatkowe parametry, jak pokazuje OWASP na przykładzie.

Ważne granice `escapeshellarg()`:

1. **Argument injection pozostaje możliwa:** Zamaskowana wartość zaczynająca się od myślnika jest mimo to odczytywana przez program docelowy jako opcja.
2. **Windows zachowuje się inaczej:** Funkcja zastępuje tam według podręcznika niektóre znaki, takie jak `%` i `"`, spacjami; wartości docierają więc zmienione.
3. **Zestaw znaków:** W zależności od ustawień locale serwera znaki spoza ASCII mogą zostać utracone.

## Wyszukiwanie miejsc w istniejącym kodzie

Wyszukanie wywołań systemowych jest szybkie i daje przejrzystą listę:

```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żde znalezisko wymaga odpowiedzi na trzy pytania: Czy wywołanie można zastąpić funkcjami PHP? Czy wpływają do niego wartości z zewnątrz? Z jakimi uprawnieniami działa proces? Ostatnie pytanie jest decydujące dla szkód w razie awarii. Proces PHP z własnym użytkownikiem i bez uprawnień zapisu do kodu wyrządzi mniej, zob. [Least Privilege: uprawnienia użytkownika serwera WWW](https://phpsos.de/pl/blog/rechte-des-webserver-benutzers/).

> **Ważne:**
> Jeśli proces PHP-FPM lub serwera WWW uruchamia powłokę, jest to silny sygnał ostrzegawczy włamania. Jak to rozpoznać, opisuje artykuł [Sygnał ostrzegawczy: serwer WWW uruchamia powłokę](https://phpsos.de/pl/blog/webserver-startet-shell/).

## Jak konkretnie pomaga PHP SOS?

- Wyszukujemy wszystkie wywołania systemowe w Państwa aplikacji, we wtyczkach i w skryptach pomocniczych.
- Zastępujemy wywołania, gdzie to możliwe, funkcjami PHP, a pozostałe przestawiamy na wywołania bez powłoki.
- Blokujemy niepotrzebne funkcje w konfiguracji PHP i sprawdzamy uprawnienia użytkownika PHP.
- Testujemy zmiany i dokumentujemy, jakie programy aplikacja nadal może wywoływać.

Kontrolę własnego oprogramowania oferujemy jako [audyt bezpieczeństwa i test penetracyjny](https://phpsos.de/pl/leistungen/sicherheitsaudit-penetrationstest/).


## Częste pytania

**Czy escapeshellarg wystarczy przeciw command injection?**  
Zapobiega temu, aby wartość została wykonana jako kolejne polecenie. Nie zapobiega temu, aby wartość została odczytana jako opcja wywoływanego programu, jeśli zaczyna się od myślnika. Dlatego dodatkowo sprawdzaj wartości i, jeśli program to obsługuje, kończ opcje za pomocą --.

**Jaka jest różnica między escapeshellarg a escapeshellcmd?**  
escapeshellarg traktuje pojedynczą wartość jako dokładnie jeden argument. escapeshellcmd maskuje tylko metaznaki w całym wierszu poleceń i dopuszcza dodatkowe parametry. Dla danych od użytkownika właściwym wyborem jest escapeshellarg, a wywołanie bez powłoki jest jeszcze lepsze.

**Czy powinienem zablokować exec i system przez disable_functions?**  
Jeśli Państwa aplikacja ich nie potrzebuje, tak. Ogranicza to szkody po włamaniu. Nie zastępuje jednak bezpiecznego kodu, ponieważ aplikacje, które potrzebują poleceń, nadal potrzebują tych funkcji.

**Jak rozpoznać, czy command injection została wykorzystana?**  
Silnym sygnałem ostrzegawczym są procesy PHP lub serwera WWW, które uruchamiają powłokę, taką jak sh lub bash. Dochodzą do tego nieznane procesy, nowe pliki w katalogach uploadu i nietypowe połączenia wychodzące.

## Źródła

- [OWASP OS Command Injection Defense Cheat Sheet (ściągawka o obronie przed command injection)](https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html)
- [Podręcznik PHP: proc_open](https://www.php.net/manual/de/function.proc-open.php)
- [Podręcznik PHP: escapeshellarg](https://www.php.net/manual/de/function.escapeshellarg.php)
