Introduzione
La creazione di contenuti dinamici che consentano all'utente di interagire con il nostro sito sono spesso alla base del successo o dell'insuccesso dei servizi che offriamo tramite il web. Maggiore è l'importanza dei dati che gestiamo, maggiore sarà anche il bisogno di sicurezza che ruota attorno ai dati stessi. La sicurezza di un sito web non viene garantita soltanto da un web server ben configurato, o da un tunnel SSL, ma deve essere implementata in maniera coscienziosa anche da chi sviluppa l'applicazione web, nel nostro caso il programmatore PHP. In questo articolo andremo a conoscere una delle più classiche tipologie di attacco legate al web, molto diffusa ma spessa sottovalutata, che va a colpire il cuore dell'applicazione web, ossia il database: si tratta dell'attacco di tipo SQL Injection. È importante tenere presente che questo fenomeno può interessare qualsiasi linguaggio di programmazione e qualsiasi DBMS, anche se gli esempi proposti faranno riferimento a PHP, con accenni a MySQL e PostgreSQL.
SQL Injection in teoria
Il problema è relativamente semplice da capire, ma è anche molto pericoloso: effettuando una query SQL costruita sulla base di input passati
da un utente, senza eseguire un controllo preventivo sullo stesso input, tale query può essere manipolata a piacimento.
L'input dell'utente
nel nostro caso può esserci trasmesso in vari modi: tramite URL (query string), tramite un form HTML oppure anche tramite un cookie costruito
su misura. Non tutti gli utenti utilizzano il nostro sito in modo "ortodosso", ad esempio cliccando su un link o compilando correttamente un
modulo, per cui i dati che ci arrivano potrebbero non rispettare le nostre aspettative.
Che tipo di problemi può portare una query manipolata arbitrariamente da un utente?
- manipolazione indesiderata dei nostri dati
- accesso indesiderato ad aree riservate
- visualizzazione di dati privati
SQL Injection in pratica
Supponiamo ad esempio di avere una variabile $id presa in input dalla query string, teoricamente di tipo intero ma non correttamente
validata. Una query del tipo $sql = "SELECT * from articoli WHERE id=$id"; potrebbe essere manipolata a piacere da un utente smaliziato, causando problemi di varia natura. Che cosa succede se $id anzichè essere un numero intero viene passata come stringa, ad esempio "1; DROP table articoli"? La query verrebbe trasformata in questo modo:
SELECT * from articoli WHERE id=1; DROP table articoli
Un altro esempio per renderci conto della gravità del problema:
$sql = "SELECT * from utenti WHERE login='$login' AND password='$password'";
Nel caso in cui non vengano effettuati controlli, una query del genere può essere comodamente modificata manipolando $login, che potrebbe diventare ad esempio "pippo' OR 1=1 --". Da notare l'apice all'interno di $login, che assume un valore particolare nella query SQL, l'operatore OR che aggiunge una clausula fittizia (1=1 è sempre vero) ed i caratteri -- che in SQL corrispondono ad un commento. La query è quindi
diventata:
SELECT * from utenti WHERE login='pippo' OR 1=1 --' AND password=''
in sostanza una query del genere restituisce il record corrispondente al login pippo, senza bisogno di inserire anche una password. Ecco quindi il bisogno di implementare una serie di controlli preventivi con lo scopo di arginare ogni possibile falla delle nostre applicazioni.
Le vulnerabilità possono essere numerose (dipende dalla fantasia dell'attaccante) e derivano solitamente da una distrazione del programmatore o comunque da una errata implementazione.
Al lato pratico, la situazione può sembrare forse meno pericolosa di quanto abbiamo visto fino a questo momento. Ad esempio MySQL non supporta gli statement multipli, per cui una query come quella del primo esempio produrrebbe soltanto un messaggio d'errore. Nessun danno quindi, ma l'errore deriva comunque da un'errata implementazione, per cui è importante correggere in qualche modo il nostro codice. Inoltre una direttiva del file di configurazione php.ini, magic_quotes_gpc, che per default è attiva, va ad eseguire l'escape (ossia
inserisce un carattere backspace, ) di alcuni caratteri potenzialmente pericolosi, come ad esempio gli apici singoli e doppi, all'interno delle variabili $_GET, $_POST e $_COOKIE. Il secondo esempio diventerebbe quindi innocuo, in quanto l'escape dell'apice lo forzerebbe ad essere interpretato non come codice SQL, ma come un normale carattere:
SELECT * from utenti WHERE login='pippo' OR 1=1 --' AND password=''
Una query del genere probabilmente non restituirà alcun risultato. Naturalmente non possiamo fare affidamento al caso, sperando che un determinato server sia configurato in un certo modo, ma dobbiamo
cercare di trovare una soluzione che ci consenta di ottenere un livello di sicurezza ragionevole, che sia magari indipendente dal tipo di DBMS adottato ma soprattutto dalla configurazione in uso.
Affrontiamo il problema
Non esiste una soluzione assoluta, che sia sicura e portabile allo stesso tempo. Lo stesso concetto di sicurezza è relativo e viene influenzato da numerosi parametri. Il nostro approccio al problema dovrà essere abbastanza elastico e dovremo adattarci alle situazioni, scegliendo di volta in volta la soluzione più opportuna.
È stato detto che il punto fondamentale è saper gestire l'input dell'utente, e sarà qui che andremo a focalizzare la nostra attenzione. A seconda dei dati che andremo a trattare, possiamo adottare più strategie per elevare il livello di sicurezza:
- Controlli sul tipo di dato
- Creazione di filtri tramite espressioni regolari
- Eliminazione di caratteri potenzialmente dannosi
- Escape di caratteri potenzialmente dannosi
Tipi di dato e type casting
Il type casting è un'operazione che forza una variabile ad essere valutata come appartenente ad un certo tipo. Nel primo esempio di query manipolata, ci aspettiamo che la variabile $id sia di tipo intero, ma per evitare sorprese, è opportuno essere sicuri di ciò:
$id = (int) $_GET['id'];
$sql = "SELECT * from articoli WHERE id=$id";
In questo modo $id avrà sicuramente un valore intero. Tendando di modificare in qualche modo la query string, un attaccante non sarà comunque in grado di eseguire istruzioni SQL arbitrarie. Il type casting può essere eseguito anche mediante la funzione settype():
settype($_GET['id'], 'int'); // forzo la variabile ad essere di tipo intero
$id = $_GET['id'];
o ancora, nel caso degli interi, con la funzione intval():
$id = intval($_GET['id']);
Un approccio leggermente diverso ma sempre legato al tipo di variabili in gioco consiste nel verificare, mediante opportune funzioni come is_int(),
is_numeric() o
gettype(), che una variabile appartenga ad un determinato tipo e comportarsi di conseguenza:
if (is_numeric($_GET['id'])) {
// ho un valore numerico, posso procedere
}
Nota: le variabili che arrivano via GET o POST sono sempre stringhe numeriche, per cui in questi casi is_numeric() va preferito rispetto ad is_int()."
Espressioni regolari
Spesso i dati che aspettiamo in input possono essere descritti da una espressione regolare. Ad esempio supponiamo che lo username di un utente registrato possa essere una stringa alfanumerica composta da un minimo di 4 ed un massimo di 12 caratteri. Per filtrare dati che non rispettano questi vincoli possiamo utilizzare la funzione preg_match() oppure la funzione ereg(), ad esempio:
if (preg_match("/^[a-z0-9]{4,12}$/i", $login)) {
// $login rispetta il parametro, per cui posso effettuare la query
}
else {
// errore
}
Per una trattazione più approfondita sulle espressioni regolari vi rimando ad un
Espressioni regolari. Con un po' di attenzione sarà possibile sbrogliare molte situazioni costruendo una espressione regolare ad hoc. Sappiamo che l'utilizzo eccessivo delle espressioni regolari tende a rallentare l'applicazione, ma la sicurezza del nostro sito vale molto più di qualche millesimo di secondo di ritardo.
Eliminazione di caratteri pericolosi
Quando il type casting non ci può servire e non troviamo un'espressione regolare che possa filtrare adeguatamente un input, possiamo decidere di eliminare eventuali caratteri pericolosi o sostituirli con codici innocui. In sostanza i caratteri potenzialmente dannosi sono quelli che hanno un significato all'interno di una query SQL, come ad esempio gli apici singoli e doppi, la virgola, il punto e virgola, e così via. In alcune situazioni tali caratteri devono essere ammessi: se un nostro utente si chiama ad esempio "Paperon de' Paperoni", l'apice fa parte del suo cognome, e sarà opportuno concederne l'inserimento in un ipotetico form di registrazione. In altri casi invece questi caratteri possono essere eliminati o sostituiti senza problemi, sfruttando una tra le funzioni
str_replace(),
preg_replace(),
ereg_replace() o
strtr(), ad esempio:
$input = str_replace("'", "", $input); // attenzione ai caratteri inseriti.
Il primo parametro di str_replace() è la stringa da cercare (nell'esempio, un apice singolo), il secondo è la stringa di sostituzione (una stringa vuota), mentre il terzo è la stringa di partenza (una ipotetica variabile $input). Un approccio del genere può comunque risultare difficile da applicare, in quanto i caratteri da controllare possono essere svariati ed i DBMS si comportano in maniera differente, per cui
sarà facile dimenticare qualche particolare.
Escape delle stringhe
Come accennato nei primi paragrafi, PHP mette a disposizione la possibilità di effettuare automaticamente l'escape di alcuni caratteri tramite
la direttiva magic_quotes_gpc
che per default è attiva e che opera sulle variabili $_GET, $_POST e $_COOKIE. Questa può essere una prima
forma di sicurezza, ma non possiamo dare per scontato che qualsiasi server su cui potrà girare la nostra applicazione abbia
questa impostazione. Per verificare lo stato di magic_quotes_gpc, possiamo usare la funzione
get_magic_quotes_gpc()
che ci restituisce 1 se l'opzione è attiva, oppure 0 se l'opzione è stata disattivata. Il risultato ottenuto con magic_quotes_gpc attivo
può essere generato manualmente anche con la funzione
addslashes()
che va ad aggiungere un carattere backspace prima di apici singoli, apici doppi, altri backspace o caratteri NUL.
Se andiamo ad effettuare un addslashes() quando magic_quotes_gpc è attivo, il risultato sarà probabilmente diverso da quello che
ci aspettiamo:
una stringa con apici doppi passata via get, post o cookie: "stringa"
viene trasformata da magic_quotes_gpc: "stringa"
aggiungendo anche addslashes() il risultato è: "stringa"
e la situazione risulta scomoda da gestire. Controllando lo stato di magic_quotes_gpc possiamo fare qualcosa di meglio:
// con magic_quotes_gpc disattivo, mi appoggio ad addslashes()
if ( ! get_magic_quotes_gpc() ) {
$_GET['variabile'] = addslashes($_GET['variabile']);
}
Un approccio più consigliato è però l'utilizzo delle funzioni specifiche relative al DBMS che stiamo utilizzando. In questo caso è opportuno fare riferimento alla pagina di manuale relativa al DBMS che ci interessa. Nel caso dei due più importanti database opensource abbiamo a disposizione le funzioni
mysql_escape_string() e
mysql_real_escape_string() (per MySQL)
oppure pg_escape_string() (per PostgreSQL).
Il loro utilizzo è grosso modo simile a quello di addslashes(), ma ci assicurano di eseguire l'escape in maniera corretta rispetto al DBMS
su cui ci appoggiamo. Dobbiamo comunque fare attenzione a tutti i casi: ad esempio le pagine di manuale ci informano che le funzioni di escape
di MySQL non lavorano sui caratteri % e _, che hanno il loro significato all'interno di un'espressione SQL che sfrutta la parola chiave LIKE. In
questo caso dobbiamo decidere come comportarci, magari facendo riferimento al paragrafo precedente (Eliminazione di caratteri pericolosi).
Conclusioni e link
Abbiamo visto come sia possibile trasformare una innocente query SQL in una istruzione potenzialmente distruttiva per il nostro database, ed
abbiamo visto quanto sia relativamente semplice effettuare una serie di controlli che hanno lo scopo di limitare i problemi. Le soluzioni che
proposte, type casting, controllo del tipo di dato, espressioni regolari, eliminazione o escape delle stringhe, possono comunque essere
integrate tra di loro.
Quando ci si trova davanti ad input diversi da quelli attesi ci sono più vie da scegliere, a seconda delle situazioni e della propria opinione
personale: è possibile utilizzare dei valori predefiniti per sostituire gli input non regolari, oppure si può scegliere di predisporre un
messaggio d'errore, o anche di costruire un sistema di logging che registri tutti i tentativi di exploit.
Indipendentemente dalla via che si decide di intraprendere, l'unico punto che dobbiamo avere ben chiaro è: controllare ogni input.
Oltre ai link alle pagina del manuale presenti nell'articolo, ecco alcuni riferimenti per approfondire l'argomento:
- Manuale PHP - database security (inglese); una pagina del manuale PHP che descrive alcuni problemi legati alla sicurezza del database.
- SQL Injection by OpenBeer (italiano, PDF); interessante paper in italiano che tratta l'argomento delle SQL injection e propone alcuni esempi per PHP/MySQL, PHP/PostreSQL ed anche ASP/MDB
- Espressioni regolari - le basi per poter utilizzare le espressioni regolari e creare quindi degli appositi filtri per i nostri dati.