Kort sagt: Command injection i PHP förhindrar ni säkrast genom att inte anropa något skal alls: Många uppgifter klarar PHP-funktioner eller bibliotek direkt. Där ett externt program behövs lämnar ni sedan PHP 7.4 kommandot som array till proc_open(), då startar PHP programmet utan skal. Värden kontrollerar ni mot en tillåtelselista, avslutar alternativ med -- och låter processen köra med minimala rättigheter.
Varför ett systemanrop har så stor makt
Bildbehandling, PDF-generering, arkivering, virusskanning, fråga efter en DNS-post: För sådana uppgifter anropar PHP-applikationer gärna externa program. Tekniskt är det bekvämt, en rad med exec() eller shell_exec() räcker. Säkerhetsmässigt är det ett av de farligaste ställena i koden.
Sätts ett kommando samman som text och körs av ett skal avgör skalet vad som är kommando och vad som är data. Hamnar ett värde utifrån där ogranskat kan det ändra kommandots betydelse. Följden är i värsta fall densamma som vid PHP2Shell: körning av godtyckliga kommandon på servern med PHP-processens rättigheter.
Skillnaden: PHP2Shell åtgärdar ni med en PHP-uppdatering. En command injection i den egna koden finns kvar efter varje uppdatering. Dessutom sitter sådana ställen ofta i hjälpskript som bara var avsedda för administratörer, till exempel för underhåll, diagnos eller importer, och senare förblir nåbara utan skydd.
Var command injection i PHP uppstår
Utsatt är varje ställe där PHP startar ett skal eller ett program. Funktionerna för det är:
| Funktion | Använder skal? | Anmärkning |
|---|---|---|
exec(), system(), passthru(), shell_exec() |
ja | Kommando som text, skalet tolkar det |
| Backtick-operatorn | ja | motsvarar shell_exec() |
popen() |
ja | Kommando som text |
proc_open() med sträng |
ja | Kommando som text |
proc_open() med array |
nej | sedan PHP 7.4, argument lämnas över direkt |
mail() med femte parametern |
indirekt | ytterligare parametrar till e-postprogrammet |
Ett typiskt osäkert mönster ser ut så här. Filnamnet kommer från förfrågan och hamnar direkt i kommandoraden:
<?php
// OSÄKERT: värde från förfrågan blir del av ett skalkommando
$datei = $_POST['datei'];
exec('convert uploads/' . $datei . ' -resize 800x800 vorschau/' . $datei);
Förstahandsval: inget skal alls
Det mest verksamma skyddet är att ersätta systemanropet. OWASP nämner det uttryckligen som första försvarslinje. För många uppgifter finns egna funktioner eller tillägg i PHP:
- Kataloger och filer:
mkdir(),copy(),rename(),unlink() - Arkiv:
ZipArchiveellerPharData - Bilder: GD eller Imagick som PHP-tillägg
- Nätverksfrågor:
dns_get_record(),gethostbyname(), cURL - Kontrollsummor:
hash_file()
Därmed försvinner frågan om vilka tecken ett skal tolkar helt. Dessutom går funktionerna för systemkommandon då att spärra i konfigurationen, som guiden Använd disable_functions rätt beskriver.
När ett externt program behövs: proc_open med array
Sedan PHP 7.4 accepterar proc_open() kommandot som array. Enligt PHP-manualen startas processen då direkt, utan omväg via skalet, och PHP tar hand om den nödvändiga maskeringen av argumenten. Tecken som semikolon eller pipe har då inte längre någon särskild betydelse.
<?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);
Tre saker gör exemplet robust: Programmet står med full sökväg fast i koden, filnamnet kontrolleras mot ett snävt mönster, och varje variabelt argument börjar med en absolut sökväg och kan alltså inte läsas som alternativ. Där ett värde står ensamt avslutar ni alternativen med --, förutsatt att programmet enligt dokumentationen stöder det, eller avvisar värden som börjar med bindestreck.
För uppgifter som regelbundet kräver externa program, till exempel bild- eller PDF-bearbetning i större omfattning, är en frikoppling värd besväret: Webbapplikationen lägger bara ett uppdrag i en kö, en separat bakgrundstjänst med egen användare och egna rättigheter utför det. PHP-processen som besvarar förfrågningar från internet behöver då inga funktioner för systemkommandon längre, och de går att spärra för den. Externa program bör dessutom ha tids- och minnesgränser så att ett enskilt uppdrag inte blockerar servern.
Ett specialfall är mail(). Den femte parametern lämnar över ytterligare alternativ till systemets e-postprogram. Värden från formulär, till exempel en avsändaradress, får aldrig hamna där ogranskade. Robustare är ett e-postbibliotek som skickar via SMTP och inte behöver något systemanrop.
escapeshellarg: använd rätt och känn gränserna
Där befintlig kod fortfarande arbetar med kommandorader är escapeshellarg() för varje enskilt värde minimiåtgärden. Funktionen sätter värdet inom citationstecken så att skalet behandlar det som exakt ett argument. escapeshellcmd() är olämplig för användarinmatning, eftersom den tillåter ytterligare parametrar, som OWASP visar med ett exempel.
Viktiga gränser för escapeshellarg():
- Argumentinjektion förblir möjlig: Ett maskerat värde som börjar med bindestreck läses ändå av målprogrammet som alternativ.
- Windows beter sig annorlunda: Där ersätter funktionen enligt manualen vissa tecken som
%och"med blanksteg; värdena kommer alltså fram förändrade. - Teckenuppsättning: Beroende på serverns locale-inställning kan icke-ASCII-tecken gå förlorade.
Hitta fynd i befintlig kod
En sökning efter systemanrop går snabbt och ger en överskådlig lista:
grep -rnE "\b(exec|system|passthru|shell_exec|popen|proc_open)\s*\(" src/ --include="*.php"
grep -rn '`' src/ --include="*.php" | grep -v '^\s*//'
Varje fynd behöver ett svar på tre frågor: Går anropet att ersätta med PHP-funktioner? Flödar värden utifrån in? Med vilka rättigheter körs processen? Den sista frågan är avgörande för skadan i skarpt läge. En PHP-process med egen användare och utan skrivrättigheter till koden åstadkommer mindre, se Least Privilege: webbserveranvändarens behörigheter.
Startar en PHP-FPM- eller webbserverprocess ett skal är det en stark varningssignal för ett intrång. Hur ni upptäcker det beskriver Varningssignal: webbservern startar ett skal.
Hur hjälper PHP SOS konkret?
- Vi söker alla systemanrop i er applikation, i tillägg och i hjälpskript.
- Vi ersätter anrop, där det går, med PHP-funktioner och ställer om de övriga till anrop utan skal.
- Vi spärrar onödiga funktioner i PHP-konfigurationen och kontrollerar PHP-användarens rättigheter.
- Vi testar ändringarna och dokumenterar vilka program applikationen fortfarande får anropa.
Granskning av egen programvara erbjuder vi som säkerhetsaudit och penetrationstest.
Vanliga frågor
Räcker escapeshellarg mot command injection?
Det förhindrar att ett värde körs som ytterligare ett kommando. Det förhindrar inte att värdet läses som ett alternativ till det anropade programmet om det börjar med bindestreck. Kontrollera därför dessutom värden och avsluta, där programmet stöder det, alternativen med --.
Vad är skillnaden mellan escapeshellarg och escapeshellcmd?
escapeshellarg behandlar ett enskilt värde som exakt ett argument. escapeshellcmd maskerar bara metatecken i en hel kommandorad och tillåter ytterligare parametrar. För användarinmatning är escapeshellarg rätt val, ett anrop utan skal är ännu bättre.
Bör jag spärra exec och system via disable_functions?
Om er applikation inte behöver dem, ja. Det begränsar skadan efter ett intrång. Det ersätter dock inte säker kod, för applikationer som behöver kommandon behöver funktionerna fortfarande.
Hur ser jag om en command injection har utnyttjats?
En stark varningssignal är PHP- eller webbserverprocesser som startar ett skal som sh eller bash. Dessutom okända processer, nya filer i uppladdningsmappar och ovanliga utgående anslutningar.