Actuele situatie: PHP2Shell. Twee kritieke PHP-kwetsbaarheden (CVSS 9.2) maken schadelijke code zonder inloggen mogelijk. Patches sinds 21-10-2026.Wat u nu moet doen →

Command injection in PHP voorkomen

Hoe ontwikkelaars command injection in PHP vermijden: systeemopdrachten vervangen door PHP-functies, proc_open zonder shell gebruiken, argumenten controleren en gevaarlijke plekken in bestaande code vinden.

Stand 6 min. leestijdVeilig ontwikkelen

Kort gezegd: Command injection in PHP voorkomt u het veiligst door helemaal geen shell aan te roepen: veel taken voeren PHP-functies of bibliotheken rechtstreeks uit. Waar een extern programma nodig is, geeft u de opdracht sinds PHP 7.4 als array door aan proc_open(), dan start PHP het programma zonder shell. Waarden controleert u tegen een lijst met toegestane waarden, opties sluit u af met -- en het proces laat u met minimale rechten draaien.

Waarom een systeemaanroep zoveel macht heeft

Beeldbewerking, pdf-generatie, archiveren, virusscan, het opvragen van een DNS-record: voor zulke taken roepen PHP-applicaties graag externe programma's aan. Technisch is dat handig, één regel met exec() of shell_exec() volstaat. Beveiligingstechnisch is het een van de gevaarlijkste plekken in de code.

Wordt een opdracht als tekst samengesteld en door een shell uitgevoerd, dan beslist de shell wat opdracht is en wat gegevens zijn. Komt een waarde van buiten ongecontroleerd binnen, dan kan die de betekenis van de opdracht veranderen. Het gevolg is in het ergste geval hetzelfde als bij PHP2Shell: uitvoering van willekeurige opdrachten op de server met de rechten van het PHP-proces.

Het verschil: PHP2Shell verhelpt u met een PHP-update. Een command injection in de eigen code blijft na elke update bestaan. Daar komt bij dat zulke plekken vaak in hulpscripts zitten die alleen voor beheerders bedoeld waren, bijvoorbeeld voor onderhoud, diagnose of imports, en later onbeschermd bereikbaar blijven.

Waar command injection in PHP ontstaat

Kwetsbaar is elke plek waar PHP een shell of programma start. De functies daarvoor zijn:

Functie Gebruikt een shell? Opmerking
exec(), system(), passthru(), shell_exec() ja Opdracht als tekst, shell interpreteert haar
Backtick-operator ja komt overeen met shell_exec()
popen() ja Opdracht als tekst
proc_open() met string ja Opdracht als tekst
proc_open() met array nee sinds PHP 7.4, argumenten worden rechtstreeks doorgegeven
mail() met vijfde parameter indirect extra parameters aan het mailprogramma

Een typisch onveilig patroon ziet er zo uit. De bestandsnaam komt uit het verzoek en belandt rechtstreeks in de opdrachtregel:

<?php
// ONVEILIG: waarde uit het verzoek wordt onderdeel van een shell-opdracht
$datei = $_POST['datei'];
exec('convert uploads/' . $datei . ' -resize 800x800 vorschau/' . $datei);

Eerste keuze: helemaal geen shell

De effectiefste bescherming is de systeemaanroep te vervangen. OWASP noemt dat uitdrukkelijk als eerste verdedigingslinie. Voor veel taken heeft PHP eigen functies of extensies:

  • Mappen en bestanden: mkdir(), copy(), rename(), unlink()
  • Archieven: ZipArchive of PharData
  • Afbeeldingen: GD of Imagick als PHP-extensie
  • Netwerkopvragingen: dns_get_record(), gethostbyname(), cURL
  • Controlesommen: hash_file()

Daarmee verdwijnt de vraag welke tekens een shell interpreteert volledig. Bovendien kunt u de functies voor systeemopdrachten dan in de configuratie blokkeren, zoals de gids disable_functions correct gebruiken beschrijft.

Als een extern programma nodig is: proc_open met array

Sinds PHP 7.4 accepteert proc_open() de opdracht als array. Volgens de PHP-handleiding wordt het proces dan rechtstreeks gestart, zonder omweg via de shell, en neemt PHP de nodige maskering van de argumenten voor zijn rekening. Tekens zoals puntkomma of pipe hebben dan geen bijzondere betekenis meer.

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

Drie dingen maken het voorbeeld robuust: het programma staat met het volledige pad vast in de code, de bestandsnaam wordt gecontroleerd tegen een nauw patroon en elk variabel argument begint met een absoluut pad, kan dus niet als optie worden gelezen. Waar een waarde alleen staat, sluit u de opties af met --, mits het programma dat volgens de documentatie ondersteunt, of wijst u waarden af die met een koppelteken beginnen.

Bij taken die regelmatig externe programma's nodig hebben, bijvoorbeeld beeld- of pdf-verwerking op grotere schaal, loont ontkoppeling: de webapplicatie plaatst alleen een opdracht in een wachtrij, een afzonderlijke achtergronddienst met eigen gebruiker en eigen rechten voert die uit. Het PHP-proces dat verzoeken van het internet beantwoordt, heeft dan geen functies voor systeemopdrachten meer nodig en die kunnen voor hem worden geblokkeerd. Externe programma's moeten bovendien tijd- en geheugengrenzen hebben, zodat een enkele opdracht de server niet blokkeert.

Een bijzonder geval is mail(). De vijfde parameter geeft extra opties door aan het mailprogramma van het systeem. Waarden uit formulieren, zoals een afzenderadres, mogen daar nooit ongecontroleerd terechtkomen. Robuuster is een mailbibliotheek die via SMTP verstuurt en geen systeemaanroep nodig heeft.

escapeshellarg: juist gebruiken en de grenzen kennen

Waar bestaande code nog met opdrachtregels werkt, is escapeshellarg() voor elke afzonderlijke waarde de minimale maatregel. De functie zet de waarde tussen aanhalingstekens, zodat de shell haar als precies één argument behandelt. escapeshellcmd() is voor gebruikersinvoer ongeschikt, omdat het extra parameters toestaat, zoals OWASP aan de hand van een voorbeeld toont.

Belangrijke grenzen van escapeshellarg():

  1. Argument-injectie blijft mogelijk: Een gemaskeerde waarde die met een koppelteken begint, wordt door het doelprogramma toch als optie gelezen.
  2. Windows gedraagt zich anders: Daar vervangt de functie volgens de handleiding bepaalde tekens zoals % en " door spaties; waarden komen dus gewijzigd aan.
  3. Tekenset: Afhankelijk van de locale-instelling van de server kunnen niet-ASCII-tekens verloren gaan.

Vindplaatsen in bestaande code opsporen

Een zoekopdracht naar systeemaanroepen is snel gedaan en levert een overzichtelijke lijst op:

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

Elke vindplaats vraagt antwoord op drie vragen: is de aanroep te vervangen door PHP-functies? Stromen er waarden van buiten in? Met welke rechten draait het proces? De laatste vraag is beslissend voor de schade in het ergste geval. Een PHP-proces met een eigen gebruiker en zonder schrijfrechten op de code richt minder aan, zie Least privilege: de rechten van de webservergebruiker.

Belangrijk

Start een PHP-FPM- of webserverproces een shell, dan is dat een sterk waarschuwingssignaal voor een inbraak. Hoe u dat herkent, beschrijft Waarschuwingssignaal: de webserver start een shell.

Hoe helpt PHP SOS concreet?

  • Wij zoeken alle systeemaanroepen in uw applicatie, in plug-ins en in hulpscripts.
  • Wij vervangen aanroepen waar mogelijk door PHP-functies en zetten de overige om naar aanroepen zonder shell.
  • Wij blokkeren niet benodigde functies in de PHP-configuratie en controleren de rechten van de PHP-gebruiker.
  • Wij testen de wijzigingen en documenteren welke programma's de applicatie nog mag aanroepen.

De controle van eigen software bieden wij aan als beveiligingsaudit en penetratietest.

Veelgestelde vragen

Is escapeshellarg genoeg tegen command injection?

Het voorkomt dat een waarde als extra opdracht wordt uitgevoerd. Het voorkomt niet dat de waarde als optie van het aangeroepen programma wordt gelezen als ze met een koppelteken begint. Controleer daarom aanvullend de waarden en sluit, waar het programma het ondersteunt, opties af met --.

Wat is het verschil tussen escapeshellarg en escapeshellcmd?

escapeshellarg behandelt een enkele waarde als precies één argument. escapeshellcmd maskeert alleen metatekens in een hele opdrachtregel en staat extra parameters toe. Voor gebruikersinvoer is escapeshellarg de juiste keuze, een aanroep zonder shell is nog beter.

Moet ik exec en system blokkeren via disable_functions?

Als uw applicatie ze niet nodig heeft, ja. Dat beperkt de schade na een inbraak. Het vervangt echter geen veilige code, want applicaties die opdrachten nodig hebben, blijven de functies nodig hebben.

Hoe herken ik of een command injection is misbruikt?

Een sterk waarschuwingssignaal zijn PHP- of webserverprocessen die een shell zoals sh of bash starten. Daarbij komen onbekende processen, nieuwe bestanden in uploadmappen en ongebruikelijke uitgaande verbindingen.

Bronnen

Direct contact

Elk uur telt. Schrijf ons nu.

Beschrijf kort het systeem, de PHP-versie en wat u opmerkt. Wij nemen contact op, brengen de situatie in kaart en werken op afstand tot het incident is afgerond en gedocumenteerd.

Directe hulp