In breve: gli input in PHP si validano con filter_var() o filter_input_array() e con regole chiare per ogni campo: tipo, intervallo, lunghezza e valori consentiti. Ciò che non corrisponde viene rifiutato, non rimaneggiato in silenzio. La validazione è importante per dati puliti e meno superficie di attacco, ma non sostituisce né i prepared statement contro la SQL injection né la codifica adeguata al contesto contro l'XSS.
Perché «filtrare tutto» non è un concetto
In molti progetti PHP più vecchi, all'inizio di ogni file si trova un ciclo che fa passare tutti i valori di $_GET e $_POST attraverso una funzione di pulizia. Dà una sensazione di sicurezza. Nella pratica a volte rimuove troppo, ad esempio gli apostrofi nei cognomi, e a volte troppo poco, perché la stessa funzione dovrebbe valere contemporaneamente per SQL, HTML e nomi di file.
Una validazione sensata funziona diversamente: per ogni campo si chiede che cosa vi sia consentito. Una quantità ordinata è un numero intero tra 1 e un limite superiore. Un campo di ordinamento è uno di tre valori fissi. Una data ha un formato determinato e viene dopo un'altra data. Tutto il resto viene rifiutato.
OWASP distingue due livelli: la verifica sintattica, cioè se un valore ha la forma giusta, e la verifica semantica, cioè se nel contesto ha senso. Entrambe appartengono al server; le verifiche nel browser servono solo all'usabilità.
Quali filtri offre PHP
L'estensione dei filtri è inclusa in PHP per impostazione predefinita. I principali filtri di validazione secondo il manuale PHP:
| Filtro | Verifica | Opzioni e note importanti |
|---|---|---|
FILTER_VALIDATE_INT |
Numero intero | min_range, max_range |
FILTER_VALIDATE_FLOAT |
Numero decimale | decimal per il separatore |
FILTER_VALIDATE_BOOL |
Valore di verità | Con FILTER_NULL_ON_FAILURE distinguibile da «falso» |
FILTER_VALIDATE_EMAIL |
Forma di un indirizzo e-mail | Non dimostra che la casella esista |
FILTER_VALIDATE_URL |
Forma di un URL | Controllare lo schema da sé, nessun dominio internazionalizzato |
FILTER_VALIDATE_IP |
Indirizzo IP | Flag per intervalli privati e riservati |
FILTER_VALIDATE_REGEXP |
Schema proprio | Ancorare lo schema con ^ e $ |
Il filtro di pulizia FILTER_SANITIZE_STRING è obsoleto da PHP 8.1. Chi lo usa ancora dovrebbe passare per la validazione a filtri adatti e usare htmlspecialchars() per l'output.
filter_var in PHP: verificare bene un modulo
Con filter_input_array() si può verificare un intero modulo in un solo passaggio rispetto a una definizione. I campi sconosciuti non vengono acquisiti:
<?php
declare(strict_types=1);
$regeln = [
'menge' => ['filter' => FILTER_VALIDATE_INT,
'options' => ['min_range' => 1, 'max_range' => 50]],
'email' => FILTER_VALIDATE_EMAIL,
'agb' => ['filter' => FILTER_VALIDATE_BOOL, 'flags' => FILTER_NULL_ON_FAILURE],
'versand' => ['filter' => FILTER_VALIDATE_REGEXP,
'options' => ['regexp' => '/^(standard|express)$/']],
];
$daten = filter_input_array(INPUT_POST, $regeln) ?? [];
$fehler = [];
foreach ($regeln as $feld => $_) {
if (!array_key_exists($feld, $daten) || $daten[$feld] === false || $daten[$feld] === null) {
$fehler[] = $feld;
}
}
if ($fehler !== []) {
http_response_code(422);
// Mostrare un messaggio di errore per campo, non elaborare gli input
}
Faccia attenzione ai confronti rigorosi con ===: un valore valido 0 o una stringa vuota non vanno scambiati con un fallimento. Perché qui i confronti laschi siano pericolosi lo spiega Type juggling: i confronti laschi come rischio di sicurezza. Come PHP metta a disposizione i dati della richiesta lo descrive Superglobali: $_GET, $_POST, $_FILES e simili.
Insidie con e-mail, URL e testi liberi
Alcuni filtri fanno meno di quanto il nome lasci supporre:
- URL:
FILTER_VALIDATE_URLaccetta anche schemi diversi da http e https. Controlli poi lo schema conparse_url()rispetto a un elenco fisso. Se l'URL viene richiamato dal server, valgono regole aggiuntive, vedi Prevenire il Server-Side Request Forgery in PHP. - Indirizzi e-mail: il filtro verifica la forma, non il possesso. Chi deve esserne certo invia un link di conferma.
- Testo libero: nomi, messaggi e indirizzi difficilmente si possono limitare in modo sensato a certi caratteri. Verifichi lunghezza e codifica dei caratteri, ad esempio con
mb_check_encoding($wert, 'UTF-8'), e in fase di output si affidi alla codifica. - Espressioni regolari: senza
^e$uno schema verifica solo se compare da qualche parte nel testo, non se corrisponde l'intero valore. - Array al posto di stringhe: un aggressore può inviare un array invece di un testo. Senza verifica del tipo ciò porta ad avvisi o a comportamenti imprevisti.
Per valori di scelta fissi come ordinamento, lingua o modalità di spedizione, una lista consentita con in_array($wert, $erlaubt, true) o un'espressione match è più semplice e più rigorosa di qualsiasi espressione regolare.
Un altro errore frequente riguarda i numeri con un significato, come prezzi, quantità o sconti. Il fatto che un valore sia un numero valido non dice nulla sul fatto che nel contesto sia consentito. Una quantità negativa, uno sconto superiore al valore della merce o un prezzo ripreso dal modulo anziché dal database sono classici errori logici. Prezzi, autorizzazioni e valori di stato appartengono perciò sempre al server e mai a campi nascosti del modulo.
JSON, webhook e importazioni: validazione oltre i moduli
Oggi molti ingressi non sono più moduli, ma richieste JSON da app, webhook di servizi di pagamento o file di importazione dei fornitori. Anche lì vale: prima leggere in sicurezza, poi verificare. OWASP consiglia di limitare dimensione e profondità di annidamento prima ancora di leggere i dati. In PHP json_decode() con una profondità massima e JSON_THROW_ON_ERROR se ne occupa in parte:
<?php
$roh = file_get_contents('php://input', false, null, 0, 65536); // al massimo 64 KB
try {
$daten = json_decode($roh, true, 16, JSON_THROW_ON_ERROR);
} catch (JsonException) {
http_response_code(400);
exit;
}
if (!is_array($daten) || !is_string($daten['bestellnummer'] ?? null)) {
http_response_code(422);
exit;
}
Per i webhook, prima della verifica del contenuto c'è la domanda se il messaggio provenga davvero dal servizio indicato. Se il servizio offre una firma, la verifichi e la confronti con hash_equals(). Per i file di importazione verifichi ogni riga come un modulo e interrompa in modo pulito in caso di errori, anziché acquisire record a metà.
I messaggi di errore dovrebbero nominare il campo interessato, ma non restituire senza filtro il valore rifiutato. Altrimenti nel messaggio di errore stesso nasce un nuovo punto di output, che va a sua volta codificato.
Che cosa la validazione non può fare
La validazione riduce la superficie di attacco, ma secondo OWASP non è la difesa principale contro l'injection. Un cognome con apostrofo è valido e deve comunque arrivare in sicurezza in un database. Ciò riesce solo con i prepared statement, come mostra la guida Prevenire la SQL injection in PHP. E un testo di commento valido diventa innocuo solo con una codifica adeguata al contesto in fase di output.
L'ordine giusto è dunque: validare alla ricezione, parametrizzare nell'elaborazione, codificare nell'output. In più, prima di tutto ciò vanno posti limiti di dimensione: richieste decisamente troppo grandi non dovrebbero nemmeno essere elaborate per intero da server web e PHP. A questo servono impostazioni come post_max_size, che dopo PHP2Shell andrebbero comunque verificate.
Come aiuta concretamente PHP SOS?
- Rileviamo tutti gli ingressi della sua applicazione: moduli, interfacce, upload, importazioni e webhook.
- Stabiliamo per ogni campo che cosa è consentito e sostituiamo le funzioni di pulizia generiche con verifiche chiare.
- Verifichiamo se accessi al database e output sono messi in sicurezza indipendentemente dalla validazione.
- Documentiamo le regole in modo che il suo team possa proseguirle con le nuove funzioni.
Una verifica sistematica è descritta nel servizio audit di sicurezza e penetration test.
Domande frequenti
Qual è la differenza tra validare e ripulire?
Validare verifica se un valore corrisponde alle aspettative e, in caso contrario, lo rifiuta. Ripulire modifica il valore, ad esempio rimuovendo caratteri. Per le decisioni di sicurezza rifiutare è quasi sempre meglio, perché un valore modificato in silenzio può generare nuovi errori.
FILTER_SANITIZE_STRING è ancora utilizzabile?
Il filtro è contrassegnato come obsoleto da PHP 8.1. Il manuale PHP raccomanda invece htmlspecialchars() in fase di output. Il codice esistente andrebbe aggiornato, perché le funzioni obsolete possono essere eliminate nelle versioni future.
FILTER_VALIDATE_URL basta per rendere sicuri i link?
No. Il filtro verifica solo la forma dell'indirizzo. Secondo il manuale un URL valido non deve necessariamente iniziare con http, quindi lo schema va controllato da sé rispetto a una lista consentita come https. Il filtro rifiuta i nomi di dominio internazionalizzati.
Devo validare anche i dati del mio database?
In fase di output sono considerati non affidabili e vengono codificati in modo adatto. Una nuova validazione ha senso se i dati provenivano originariamente dall'esterno e ora vengono usati in un nuovo contesto, ad esempio come nome di file o parte di un comando.
La validazione aiuta contro PHP2Shell?
No. PHP2Shell riguarda l'elaborazione delle richieste nell'interprete PHP, prima ancora che giri il codice della sua applicazione. Contro di esso aiuta solo l'aggiornamento di PHP con il successivo riavvio di tutti i processi PHP.