Sicurezza e siti web: come trovare le cause di code injection, tecniche di validazione
E così, dopo aver analizzato il funzionamento “completo” di un sito web, abbiamo scoperto quanto sia vulnerabile, e da quanti punti lo sia. Abbiamo parlato di code injection, e visto le origini di questo tipo di debolezza, magari anche scoperto che alcuni nostri siti sono vulnerabili. Dato che il compito di questi articoli non è solo valutare le debolezze, ma anche irrobustire le difese – come trovare ed eliminare le vulnerabilità dai nostri siti?
“Tutto è nelle mani del nemico. Non fidarti di nessuno” – Fox Mulder sarebbe stato un eccellente programmatore!
Di sicuro, una notizia di cattivo auspicio. La buona notizia è che possiamo evitare per intero il code injection controllando accuratamente il codice, e praticando tecniche di programmazione note come validazione, escaping e indirezione.
Nel resto dell’articolo, per mantenere quanto possibile la semplicità farò riferimento esclusivamente a tecnologie basate su Javascript e php, ma i concetti sono assolutamente estensibili anche a ASP e VBScript, JSP, Java, Ruby o qualunque altro linguaggio usato a livello di browser ed a livello di server.
Facciamoci aiutare – dal server
Per prima cosa, assicuriamoci di una cosa molto importante – che il nostro server PHP abbia il flag register_globals su off. Molti ne avranno sentito parlare ma, in realtà, a cosa serve e perché è “pericoloso” se settato su on?
Il flag register_globals indica come l’interprete PHP tratta le variabili globali: principalmente, tutte quelle variabili che provengono dalla url, o vengono passate tramite post, o anche i cookies e le variabili di sessione (in pratica, tutte le variabili esterne), definite come globali perché accessibili da ogni funzione senza essere dichiarate.
Se il flag è impostato su on, come era nelle prime versioni di PHP, tutte le variabili esterne diventano automaticamente variabili php. Se è invece, come di norma nelle ultime versioni, impostato su off, le variabili esterne sono accessibili esclusivamente tramite i vari array $_POST, $_GET, $_REQUEST, $_SESSION, etc.
Perchè questo è un problema? Perché molto spesso in php si usa il valore di una variabile senza inizializzarla, perché sappiamo che il php la inizializza “come vuota” per noi. Un esempio è il codice seguente:
if ($_REQUEST["flag"] == "yes") $messaggio = "flag attivo"; echo $messaggio;
L’idea è che se invochiamo questo script così:
https://www.vergognosi.it/script.php?flag=yes
mi viene visualizzata la scritta “flag attivo”. Se invece lo invocassimo così:
https://www.vergognosi.it/script.php?flag=no
non mi verrebbe visualizzato niente. Ora, se il flag register_globals fosse su on, io potrei fare una cosa di questo genere:
https://www.vergognosi.it/script.php?messaggio=imprevisto
e mi visualizzerebbe la scritta “imprevisto”!
Questo perchè il PHP registrerebbe la variabile “messaggio”, che ho passato nell’url, automaticamente come variabile globale PHP. E non avendo inizializzato tale variabile prima, ho effettivamente creato un punto vulnerabile al code injection. Ora, ovviamente nel caso di cui sopra i danni sono limitati, ma immaginiamo cosa succederebbe se la variabile di cui sopra fosse usata per comunicare con un database o scrivere un file: a questo punto, avrei creato un passaggio diretto dall’url al mio database!
Si può notare immediatamente che questa tecnica d’aggressione è efficace soprattutto quando si conosce il codice php. Già sappiamo che basare la sicurezza sulla segretezza è un errore: il codice potrebbe essere stato letto, magari da qualche personaggio poco fidato nella web farm, o esposto da qualche problema del server web – o, addirittura, essere visibile a tutti perché open source.
In certi casi, la compromissione avviene banalmente perché il programmatore ha condiviso parti di codice su forum per programmatori, condivisione da cui elementi malintenzionati sono in grado di capire lo schema con cui inventiamo i nomi delle variabili e dedurli per applicarli ad altre nostre creazioni, e notare i lati deboli del nostro stile di programmazione per poi forzarli.
Molto spesso, il problema è subdolo:
// // CODICE NON SICURO, NON USARE! // if ($_REQUEST["settore"] == "commerciale") $email = "commerciale@vergognosi.it"; else if ($_REQUEST["settore"] == "marketing") $email = "marketing@vergognosi.it"; if ($email != "") { echo "Specificare una email!"; exit; } mail($email, "Contatto ricevuto", "Contatto ricevuto da $_REQUEST[cognome] $_REQUEST[nome]");
In questo caso, lo script sarebbe (quasi, a parte la mailto … ma ne parleremo più avanti) sicuro se il flag register_globals fosse su off, ma se è su on, a me basta invocarlo così:
https://www.vergognosi.it/script.php?email=il%20tuo%20indirizzo@email
e far felice qualche spammer: ho appena creato un mail gateway, che posso sfruttare per spedire mail a mia insaputa, a mio costo (o, peggio, del cliente che è molto meno accomodante …) a mezzo mondo. I mail gateway tramite php, per inciso, sono tutt’altro che rari o non usati nel mondo dello spam.
Una cosa importante da notare: il flag register_globals non è “cattivo” in se. Nessuno dei problemi di cui sopra sorgerebbe se io inizializzassi tutte le variabili, che rimane comunque una pratica da seguire assolutamente. Mettendolo su off, però, sono protetto da questo tipo di problemi – eliminando i problemi alla radice, faccio solo del bene al mio codice.
Una checklist per il server
- verifichiamo, tramite la funzione phpinfo, che il flag register_globals sia su off;
- se è su on, settiamolo a off. A livello Apache (non possiamo farlo altrove!), basta aggiungere nell’.htaccess sulla directory principale del nostro sito: php_value register_globals off
- se per qualche ragione non posso metterlo su off (verifichiamo sempre tramite phpinfo che il valore cambi, e se siamo puntigliosi verifichiamo all’avviamento dei nostri script in automatico), chiediamo alla web farm di farlo per noi;
- se per qualche ragione non possono farlo a livello globale (vecchi siti scritti in PHP4 potrebbero non funzionare) e non posso farlo a livello locale perchè magari non posso modificare i .htaccess (e, soprattutto, non posso passare a provider migliori!), controlliamo ed inizializziamo tutte le variabili in uso dal nostro php. Un aiuto può venirci tramite la funzione error_reporting, messa al massimo dettaglio ci dirà se vengono usate variabili non inizializzate.
Faccio notare che l’uso del flag di error_reporting al massimo dettaglio è una pratica salutare e consigliata, anche laddove non ci siano problemi nella gestione del flag di register_globals. Un codice che non produce errori di nessun genere è un codice migliore, quindi meno soggetto ad imprevisti e, di conseguenza, meno suscettibile ad aggressioni.
Tecniche di programmazione
Una volta che ci siamo assicurati la “collaborazione” del server, è ora di mettere ordine al nostro codice. Parlo di mettere in ordine ma, normalmente, questa fase viene effettuata durante la scrittura del codice, non dopo: è parte integrante. L’idea di scrivere il codice prima “alla veloce” per poi essere rinforzato dopo è foriera di problemi – non farlo se non sei davvero costretto e solo per cose molto piccole e molto temporanee, è meglio se integri da subito qualche buona tecnica e “pensi” il codice già al livello di paranoia necessario.
Le tre tecniche che andremo a vedere sono:
- la validazione, ovvero ci assicuriamo che i dati siano validi sia da un punto di vista sintattico, numeri fatti di cifre, nomi fatti di lettere, che dove possibile semantico: date composte da numeri validi, indirizzi di mail composti dalle varie componenti, etichette esistenti che corrispondono a quelle che vogliamo, stringhe non vuote dove non devono essere vuote. Sebbene non sia propriamente corretto come definizione, questa fase include tacitamente anche la conversione dei valori: se leggo un numero nel mio codice php, lo converto in formato numerico, se leggo una data in Javascript la converto in oggetto Date: non li lascio rispettivamente come stringa o array di numeri;
- l’escaping, ovvero sostituire simboli e caratteri che hanno un significato particolare in un dato linguaggio, pensiamo ad esempio ai caratteri ‘ e ” nelle stringhe SQL e Javascript, o a < > e & in HTML, con quelle che vengono definite storicamente “sequenze di escape”: ad esempio, nelle stringhe SQL si sostituiscono tutti i ‘ con \’. Quel primo \ è detto carattere di escape. A livello di HTML, per esempio, la sequenza di escape di < è <
- l’indirezione, che ci permette di “isolare” i dati di input dal resto del nostro codice sostituendoli tramite tabelle di conversione. Si tratta di una tecnica estremamente efficace, sebbene di descrizione piuttosto oscura: sarà molto chiara una volta mostrata con esempi.
Generalmente, si usa la validazione durante la lettura dei dati, l’escaping durante il passaggio di linguaggio, mentre l’indirezione è utile in entrambe le fasi.
Validazione
Il primo punto di aggressione per il code injection è durante la lettura dei dati. Consideriamo una bella pagina in PHP, possiamo dire che la lettura dei dati avviene:
- usando gli array $_GET, $_POST, $_REQUEST. Effettivamente leggo i dati che mi ha passato l’utente;
- ovviamente, leggendo un file o un html esterno (fopen, popen, fread, etc);
- caricando i record che risultano da un’interrogazione ad un database.
Per semplicità, ora faremo solo esempi che riferiscono a $_REQUEST, che è spesso la sorgente in assoluto più importante, oltre che la prima che dobbiamo gestire.
Andiamo a cercare tutti gli usi effettuati con $_REQUEST: ogni uso è un possibile punto d’aggressione per il code injection. Per eliminare il punto d’aggressione, i valori presi direttamente da $_REQUEST:
- non devono apparire nei nomi di files o di url;
- non devono apparire nelle query SQL;
- non devono apparire nell’output HTML;
- non devono assolutamente apparire nelle chiamate al sistema operativo o alla shell (system, passthru, etc);
- se vengono passati ad una variabile, interi o concatenati, questa deve essere controllata allo stesso modo!
- se (orrore) viene usato come parametro di una chiamata eval …. sala mensa, appesi per i piedi, frustate …
Quindi, cosa dobbiamo fare con questi valori se non possiamo far apparentemente nulla?
Dobbiamo validarli, ovvero garantire che i valori che contengono siano quelli che pensiamo che siano, quindi validi, e dove possibile anche convertirli. Il php disponde di un eccellente set per il controllo dei valori delle variabili, che vi consiglio di studiare ed usare anziché scrivere la vostra versione dove possibile: https://it.php.net/manual/en/ref.var.php
Ad esempio, se mi aspetto un numero intero, controlliamo che sia un numero e convertiamolo, eventualmente verificando che i valori siano validi:
if (is_numeric($_REQUEST["dado"])) $dado = intval($_REQUEST["dado"]) else exit; if (($dado<1)||(6<dado)) exit;
Se mi aspetto un’etichetta, come nel caso dei pulsanti radio del guestbook, controlliamo che siano validi:
switch ($_REQUEST["quanto"]) { case "tanto": case "mah": $quanto = $_REQUEST["quanto"]; default: // mi arrabbio, oppure setto $quanto al valore di default error_log("qualcuno ci sta provando col mio guestbook!!1!11!"); exit; }
oppure:
$commenti_validi = array("tanto"=>true, "quanto"=>true); if (array_key_exists($_REQUEST["quanto"], $commenti_validi)) $quanto = $_REQUEST["quanto"]; else { error_log("qualcuno ci sta provando col mio guestbook!!1!11!"); exit; }
Altri controlli validi possono essere:
- la lunghezza dei campi: un errore molto frequente è fidarsi del fatto che possiamo specificare lunghezze massime dei campi di testo nelle form tramite html. Come già detto, mai fidarsi di quello che riceviamo dalla pagina html. Questo vale anche nel caso si debba semplicemente essere sicuri che un campo sia stato compilato, quindi almeno di lunghezza 1;
- la validità delle date: php dispone di una funzione di verifica chiamata checkdate: usala, non scriverti la tua;
- l’esistenza dei valori inseriti in tabelle secondarie. Ad esempio, il classico select con la lista delle nazioni nelle form di contatto- verificate che il valore ritornato sia corretto ed esistente, non fidatevi del Javascript.
Espressioni regolari
Uno dei tool in assoluto più potente per validare i dati a livello di sintassi, sia in php che in Javascript, è l’espressione regolare (indicata spesso con la sola sigla RE). L’argomento è stato trattato recentemente molto bene da JustB in questo articolo, a cui lascio volentieri il compito di spiegare in dettaglio la natura ed i dettagli dello strumento. Tra gli usi possibili delle espressioni regolari nel contesto della validazione troviamo:
- la validazione di un indirizzo email;
- la validazione di una URL;
- la validazione di codice fiscale e partita iva.
Parliamo, chiaramente, di validazione sintattica (numeri e lettere al posto giusto e nella giusta quantità): per quanto potente, ricordiamoci sempre che non garantisce che l’email esista, l’URL sia raggiungibile o che codice fiscale o partita iva siano validi: per le verifiche semantiche, quelle possibili, occorre usare codice specializzato la cui spiegazione esce dal contesto di questo articolo.
Quando usare la validazione?
La regola dice che occorre validare i valori ogni volta che vengono letti, quindi sia quando vengono letti da $_REQUEST che quando vengono letti da database o files (che potrebbero essere stati compromessi). Ora, chiunque abbia scritto più di un paio di applicazioni in php capisce benissimo che un approccio integralista di questo genere diventa rapidamente difficoltoso da gestire: tanti controlli da fare, quindi tanta complicazione, e la complicazione genera spesso problemi aggiuntivi, sino a diventare un problema di paranoia.
È chiaro che la decisione di quanto essere paranoici deve dipendere da molti fattori, inclusi la criticità dell’ambiente (banche, obiettivi a rischio, siti ad alto traffico), il budget allocato, il valore intrinseco dei dati trattati.
Un errore da evitare è validare in più posti, e più di una volta. Concentra tutto il codice di valutazione insieme, variabile per variabile, e nella stessa parte del codice, così validerai solo una volta. Chiaramente, se si devono validare più sorgenti (es. $_REQUEST, files) non si potrà avere solo un punto di validazione, ma si può sempre validare un’intera sorgente di dati in un punto solo. Segui questo consiglio e quando, magari tra qualche anno, andrai a rivederti il codice, sarai felice di averlo fatto!
Ha senso validare in Javascript?
Questo è un argomento di discussione perennemente aperto, dove esistono diversi pareri a volte molto discordanti.
Il dato oggettivo: la validazione deve essere fatta sempre, in ogni caso, nel lato server, quindi in php. Non ci si deve fidare dei dati ricevuti dal browser, quindi non ci si deve fidare delle validazioni fatte in Javascript. MAI! Se avete letto le parti precedenti, le ragioni dietro a questo concetto dovrebbero essere assolutamente chiare.
Il dato soggettivo: per questioni di efficienza, per diminuire il numero di chiamate effettuate verso il sito, può aver senso fare validazione anche in Javascript. Ricordiamoci della minoranza di malintenzionati, certo, ma non a scapito della maggioranza di utenti veri: se validiamo già in Javascript la nostra applicazione sarà più veloce e più brillante nell’interazione, invece che attendere il responso dal server.
Ma … c’è un ma. Questo comporta necessariamente una duplicazione del codice: devo verificare una stringa vuota, un indirizzo email corretto, l’esistenza di certi campi sia in Javascript che in php. Ogni duplicazione di codice è soggetta a problemi di sincronismo – se modifico una parte, devo ricordarmi di modificare anche l’altra, se tolgo una validazione la devo togliere anche dall’altra parte. Ogni sincronismo è una sorgente molto probabile di errori o di problemi di sicurezza, oltre che un costo.
Il mio consiglio è di affrontare il problema caso per caso. Se i tempi di risposta per una validazione effettuata solo in php sono accettabili ed il traffico aggiuntivo generato non è un problema, effettuiamola solo in php. Se parliamo di siti molto trafficati o di web farm sature, usiamo la doppia validazione – siamo già in un contesto estremo, ed i costi possono essere accettabili a fronte di miglioramenti significativi nelle prestazioni.
In molti casi, la soluzione ibrida è la migliore: a fronte di una validazione completa effettuata in php, teniamo una validazione “base” a livello Javascript. Per esempio, nelle form che richiedono l’approvazione per il trattamento della privacy, una verifica in Javascript del settaggio del checkbox relativo è un ottimo sistema per ridurre il traffico sul server web. Settaggio che viene comunque controllato dal server, assieme al resto dei campi da validare.
Conclusione
Data l’ampiezza dell’argomento, mi fermo qui, prima di rischiare di confonderti le idee con nuovi argomenti. Nel prossimo articolo proseguirò con le altre due tecniche.
Nel frattempo, ti lascio digerire la teoria sulla validazione, e ti invito a parlare della tua esperienza con questa seducente creatura informatica. Esistono moltissimi approcci pratici al problema validazione, che includono tanto le tecniche object oriented quanto i vecchi sistemi procedurali: quale è il tuo, come affronti la validazione nei tuoi siti e da che parte stai – tutta la validazione in php, o usi un approccio ibrido?
Indice
Sicurezza e siti web
Introduzione
Cosa significa, e perchè non devi sottovalutarla su internet?
Cosa si nasconde dietro al tuo sito?
Mettere in sicurezza il codice
Code injection: cosa sono e dove si nascondono
Come trovare le cause di code injection, tecniche di validazione.
8 commenti
Trackback e pingback
[...] This post was mentioned on Twitter by nando pappalardo and Maria Pia De Marzo, Your Inspiration Web. Your Inspiration…
[...] 02) Sicurezza e siti web: come trovare le cause di code injection, tecniche di validazione [...]
[...] visto nell’articolo precedente che un’ottima fase di validazione, oltre che migliorare significativamente la qualità dei [...]