In breve: la command injection in PHP si previene nel modo più sicuro non chiamando affatto una shell: molti compiti li svolgono direttamente funzioni o librerie PHP. Dove serve un programma esterno, da PHP 7.4 si passa il comando come array a proc_open(), e PHP avvia il programma senza shell. I valori si verificano rispetto a una lista consentita, le opzioni si chiudono con -- e il processo gira con privilegi minimi.
Perché una chiamata di sistema ha tanto potere
Elaborazione di immagini, generazione di PDF, archiviazione, scansione antivirus, interrogazione di un record DNS: per compiti simili le applicazioni PHP richiamano volentieri programmi esterni. Tecnicamente è comodo, basta una riga con exec() o shell_exec(). Dal punto di vista della sicurezza è uno dei punti più pericolosi del codice.
Se un comando viene composto come testo ed eseguito da una shell, è la shell a decidere che cosa sia comando e che cosa siano dati. Se un valore dall'esterno vi giunge senza verifica, può cambiare il significato del comando. La conseguenza, nel caso peggiore, è la stessa di PHP2Shell: esecuzione di comandi arbitrari sul server con i privilegi del processo PHP.
La differenza: PHP2Shell si risolve con un aggiornamento di PHP. Una command injection nel proprio codice resta dopo ogni aggiornamento. A ciò si aggiunge che simili punti si trovano spesso in script ausiliari pensati solo per gli admin, ad esempio per manutenzione, diagnosi o importazioni, e che in seguito restano raggiungibili senza protezione.
Dove nasce la command injection in PHP
È a rischio ogni punto in cui PHP avvia una shell o un programma. Le funzioni a tal fine sono:
| Funzione | Usa una shell? | Nota |
|---|---|---|
exec(), system(), passthru(), shell_exec() |
sì | Comando come testo, la shell lo interpreta |
| Operatore backtick | sì | Corrisponde a shell_exec() |
popen() |
sì | Comando come testo |
proc_open() con stringa |
sì | Comando come testo |
proc_open() con array |
no | Da PHP 7.4, gli argomenti vengono passati direttamente |
mail() con il quinto parametro |
indirettamente | Parametri aggiuntivi al programma di posta |
Uno schema tipicamente insicuro è questo. Il nome del file proviene dalla richiesta e finisce direttamente nella riga di comando:
<?php
// INSICURO: un valore della richiesta diventa parte di un comando shell
$datei = $_POST['datei'];
exec('convert uploads/' . $datei . ' -resize 800x800 vorschau/' . $datei);
Prima scelta: nessuna shell
La protezione più efficace è sostituire la chiamata di sistema. OWASP lo indica espressamente come prima linea di difesa. Per molti compiti PHP ha funzioni o estensioni proprie:
- Directory e file:
mkdir(),copy(),rename(),unlink() - Archivi:
ZipArchiveoPharData - Immagini: GD o Imagick come estensione PHP
- Interrogazioni di rete:
dns_get_record(),gethostbyname(), cURL - Checksum:
hash_file()
Così scompare del tutto la questione di quali caratteri interpreti una shell. Inoltre le funzioni per i comandi di sistema si possono poi bloccare nella configurazione, come descrive la guida Usare bene disable_functions.
Quando serve un programma esterno: proc_open con array
Da PHP 7.4 proc_open() accetta il comando come array. Secondo il manuale PHP il processo viene allora avviato direttamente, senza passare dalla shell, e PHP si occupa della necessaria mascheratura degli argomenti. Caratteri come il punto e virgola o la pipe non hanno più alcun significato speciale.
<?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 cose rendono robusto l'esempio: il programma è fissato nel codice con il percorso completo, il nome del file viene verificato rispetto a uno schema stretto e ogni argomento variabile inizia con un percorso assoluto, quindi non può essere letto come opzione. Dove un valore sta da solo, si chiudono le opzioni con --, purché il programma lo supporti secondo la documentazione, oppure si rifiutano i valori che iniziano con un trattino.
Per compiti che richiedono regolarmente programmi esterni, ad esempio l'elaborazione di immagini o PDF su larga scala, vale la pena di un disaccoppiamento: l'applicazione web inserisce soltanto un incarico in una coda, e un servizio in background separato, con utente e privilegi propri, lo esegue. Il processo PHP che risponde alle richieste da Internet non ha più bisogno di funzioni per i comandi di sistema, e per esso si possono bloccare. I programmi esterni dovrebbero inoltre avere limiti di tempo e di memoria, affinché un singolo incarico non blocchi il server.
Un caso particolare è mail(). Il quinto parametro passa opzioni aggiuntive al programma di posta del sistema. Valori provenienti da moduli, ad esempio un indirizzo del mittente, non devono mai finirvi senza verifica. Più robusta è una libreria di posta che invia via SMTP e non richiede alcuna chiamata di sistema.
escapeshellarg: usarlo bene e conoscerne i limiti
Dove il codice esistente lavora ancora con righe di comando, escapeshellarg() per ogni singolo valore è la misura minima. La funzione racchiude il valore tra virgolette, così che la shell lo tratti come esattamente un argomento. escapeshellcmd() è inadatto agli input degli utenti, perché ammette parametri aggiuntivi, come OWASP mostra con un esempio.
Limiti importanti di escapeshellarg():
- L'argument injection resta possibile: un valore mascherato che inizia con un trattino viene comunque letto dal programma di destinazione come opzione.
- Windows si comporta diversamente: lì la funzione sostituisce secondo il manuale determinati caratteri come
%e"con spazi; i valori arrivano quindi modificati. - Set di caratteri: a seconda dell'impostazione locale del server, i caratteri non ASCII possono andare persi.
Trovare i punti sensibili nel codice esistente
Una ricerca delle chiamate di sistema è rapida e fornisce un elenco gestibile:
grep -rnE "\b(exec|system|passthru|shell_exec|popen|proc_open)\s*\(" src/ --include="*.php"
grep -rn '`' src/ --include="*.php" | grep -v '^\s*//'
Ogni occorrenza richiede una risposta a tre domande: la chiamata si può sostituire con funzioni PHP? Vi confluiscono valori dall'esterno? Con quali privilegi gira il processo? L'ultima domanda è decisiva per il danno nel caso grave. Un processo PHP con utente proprio e senza permessi di scrittura sul codice fa meno danni, vedi Least Privilege: i permessi dell'utente del server web.
Se un processo PHP-FPM o del server web avvia una shell, è un forte segnale d'allarme di un'intrusione. Come riconoscerlo lo descrive Segnale d'allarme: il server web avvia una shell.
Come aiuta concretamente PHP SOS?
- Cerchiamo tutte le chiamate di sistema nella sua applicazione, nei plugin e negli script ausiliari.
- Sostituiamo, dove possibile, le chiamate con funzioni PHP e portiamo le restanti a chiamate senza shell.
- Blocchiamo le funzioni non necessarie nella configurazione di PHP e verifichiamo i privilegi dell'utente PHP.
- Testiamo le modifiche e documentiamo quali programmi l'applicazione può ancora richiamare.
La verifica del software proprio la offriamo come audit di sicurezza e penetration test.
Domande frequenti
escapeshellarg basta contro la command injection?
Impedisce che un valore venga eseguito come comando ulteriore. Non impedisce che il valore venga letto come opzione del programma chiamato, se inizia con un trattino. Per questo si verificano anche i valori e, dove il programma lo supporta, si chiudono le opzioni con --.
Qual è la differenza tra escapeshellarg ed escapeshellcmd?
escapeshellarg tratta un singolo valore come esattamente un argomento. escapeshellcmd maschera soltanto i metacaratteri in un'intera riga di comando e ammette parametri aggiuntivi. Per gli input degli utenti escapeshellarg è la scelta giusta, una chiamata senza shell è ancora meglio.
Dovrei bloccare exec e system con disable_functions?
Se la sua applicazione non ne ha bisogno, sì. Limita il danno dopo un'intrusione. Non sostituisce però un codice sicuro, perché le applicazioni che richiedono comandi continuano ad avere bisogno di quelle funzioni.
Come riconosco se una command injection è stata sfruttata?
Un forte segnale d'allarme sono processi PHP o del server web che avviano una shell come sh o bash. A ciò si aggiungono processi sconosciuti, nuovi file nelle cartelle di upload e connessioni in uscita insolite.