SQL-Injection: cos’è e come difenderci

Delicious Delicious Stumble Upon Stumble Upon | SQL-Injection: cos’è e come difenderci

febbraio 3rd, 2009 by Simone D'Amico in: php, sicurezza | No Comments »

Continua la serie degli articoli sulla sicurezza dei nostri progetti PHP. L’altra volta vi ho parlato di come rendere più sicure le sessioni PHP, oggi vi parlerò di uno degli attacchi più diffusi al nostro database: SQL-Injection.

La SQL injection è una tecnica dell’hacking mirata a colpire le applicazioni web che si appoggiano su un database di tipo SQL. Questo exploit sfrutta l’inefficienza dei controlli sui dati ricevuti in input ed inserisce codice maligno all’interno di una query SQL. Le conseguenze prodotte sono imprevedibili per il programmatore: l’Sql Injection permette al malintezionato di autenticarsi con ampi privilegi in aree protette del sito (ovviamente, anche senza essere in possesso delle credenziali d’accesso) e di visualizzare e/o alterare dati sensibili.

Wikipedia

In parole povere quindi l’SQL injection sfrutta bug di programmazione nell’immissione di dati input per poter avere pieno accesso al database e quindi a tutti i dati dell’applicazione web. La prima cosa che ci viene in mente quindi leggendo queste parole, anche non sapendo come funzionano attacchi di questo tipo, è sicuramente rendere più sicure le query che verranno fatte al database.

Diciamo che grosso modo le query che un’applicazione web esegue sul database si suddividono in due tipi:

  • Query che vanno a modificare il database: INSERT, UPDATE, DELETE
  • Query che interrogano il database senza modificarne i dati al suo interno:  SELECT

Sono tante le operazioni che si possono effettuare con il database ma questi quattro comandi sono sufficienti per avere una buona gestione di tutti i dati.

Su una comune applicazione web l’utente utilizza (ovviamente da qui in poi dò per scontato che per “usa” si intende a livello di codice) molto meno il primo tipo di query mentre sono più le operazioni che restituiscono un risultato. La maggior parte dei progettatori di db utilizzano come chiave univoca un id numerico,  quindi la maggior parte delle query che vengono eseguie su un database saranno di questo tipo:

SELECT * FROM 'prodotti' WHERE id = 123;

Una query di questo genere quindi si aspetta che gli vengano passati solo e soltanto parametri di tipo intero. Allora perchè non dire subito al PHP che tipo di dato passare alla query? Siamo ottimisti e supponiamo che “casualmente” un utente sbagli a digitare e, anzichè chiamare una pagina di tipo:

http://sito.it/prodotti.php?id=123

chiami qualcosa del tipo:

http://sito.it/prodotti.php?id=123abc

Al momento siamo ancora molto ottimisti e diamo per scontato che non sia un utente malintenzionato ma solo “sbadato”. Cosa succede al database?  Al database viene passata una query con id=”123abc” e quindi sicuramente il risultato della query sarà nullo perchè non esiste un id di quel genere. Un consiglio quindi è quello di dire subito al PHP che deve passare un valore numerico alla query e non la stringa immessa da parametro GET che potrebbe contenere errori. E’ quindi sufficiente effettuare un banale casting alla variabile prima di passarla al db.

<?php
//....vari controlli
if( isset($_GET['id'] ) $id = (int)$_GET['id'];
//...effettuiamo la SELECT con $id
?>

Il codice precendente non fa altro che forzare la stringa ricevuta come parametro GET ad essere passata come un intero. Quindi una stringa come “123abc” viene passata come “123″.

Quello che ho appena spiegato è un banale consiglio che dovrebbe essere usato a prescidere dalle intenzioni dei malintenzionati che potrebbero sfruttare questo “bug” in modo molto più dolorosi di un semplice errore di battitura.

Un altro errore che molti fanno è quello di passare qualunque dato al database senza effettuare alcun tipo di controllo dei dati. Ripeto, ancora stiamo supponendo che gli utenti siano tutti di buone intenzioni e che quindi non vogliano fare danni alla nostra applicazione. Supponiamo che la nostra query sia qualcosa del genere:

SELECT * FROM 'articoli' WHERE autore = 'Giuseppe Rossi';

Non stiamo chiedendo nulla di strano al nostro database; semplicemente vorremo che ci restituisse tutti gli articoli scritti da Giuseppe Rossi. Un esempio di chiamata della pagina quindi potrebbe essere:

http://sito.it/articoli.php?author=Giuseppe Rossi

E se l’autore si chiamasse Simone D’Amico? Nulla di strano potreste dire. E invece c’è qualcosa di molto strano da non trascurare! Proviamo ad eseguire la pagina passandogli quel tipo di dato senza effettuare controlli. Cosa succede? Succede che l’apostrofo potrebbe creare danni sia a livello di codice PHP che a livello di query. Il primo caso restituisce un errore di stringa e tutto passa quasi indolore ma se passiamo un escape strano al database si potrebbero avere molti più problemi che tra poco inizieremo ad analizzare.

Cosa fare quindi? Non dobbiamo assumere autori con l’apostrofo nel cognome? Non mi sembra il massimo come alternativa! :) Una soluzione molto più semplice è quella di far “filtrare” la stringa ad una funzione apposita prima di passarla al database.

La funzione si chiama mysql_escape_string( string $unescaped_string ) e il seguente esempio spiega come usarla:

<?php
//....
$stringa = mysql_escape_string( $_GET['autore'] );
//....passo alla SELECT la stringa $autore;
?>

L’esempio di sopra dovrebbe quindi passare al database qualcosa del genere:

SELECT * FROM 'articoli' WHERE autore = 'Simone D\'Amico';

Gli unici caratteri per cui non effettua l’escape sono: % (percentuale) e _ (underscore).

Giunti a questo punto molti si saranno annoiati perchè mi sono effettivamente dilungato molto nell’introduzione all’articolo ma purtroppo molte persone non fanno caso a questi particolari senza sapere che sono la base per garantire un minimo di sicurezza all’applicazione web e agli utenti che ne fanno uso.

1) Doppia Query

Entriamo più nel dettaglio con l’esempio più banale di SQL-Injections che, fortunatamente, ad oggi molti database impediscono in automatico.

Torniamo alla nostra pagina:

http://sito.it/prodotti.php?id=123

La query quindi sarà:

SELECT * FROM 'prodotti' WHERE id = 123;

Il malintenzionato potrebbe invece passare alla nostra pagina qualcosa del genere:

http://sito.it/prodotti.php?id=123;DROP table prodotti

La query quindi diverrà:

SELECT * FROM 'prodotti' WHERE id = 123; DROP table prodotti;

La (ovvia) poca fantasia nel definire il nome della tabella “prodotti” porterebbe in un batter d’occhio un malintenzionato a far eseguire una doppia query che cancella definitivamente la nostra tabella e senza neppure troppa fatica! Come detto prima fortunatamente questa tecnica non è più realizzabile perchè molti database non consentono statement multipli, per cui una query come quella del primo esempio produrrebbe soltanto un messaggio d’errore.

2) Condizione WHERE sempre vera

Un altro metodo molto utilizzato è quello di rendere il WHERE della SELECT sempre vero. Come vedremo nei prossimi esempi le tecniche per farlo sono davvero tante, basta solo un pò di fantasia! :)

Prendiamo in esempio una query di questo tipo:

SELECT * FROM 'users' WHERE email='pippo@example.it';

Nella pagina php il codice sarà:

<?php
//....
$query = "SELECT * FROM 'users' WHERE email='{$_GET['email']}'";
?>

L’utente malintenzionato potrebbe passare alla pagina una stringa come la seguente (notare la disposizione degli apici):

http://sito.it/users.php?email=pippo@example.it' OR 'x' = 'x

Come possiamo notare non fa altro che confrontare la stringa ‘x’ con sè stessa quindi ovviamente la query risulterà sempre vera. Supponiamo che quella pagina mostri i risultati a schermo potremmo visualizzare il contenuto della tabella utenti senza alcuna fatica. A seconda di come la query sia stata strutturata potrebbe restituire anche un solo risultato ma in quel caso saremmo in un bel guaio ugualmente. La query restituirebbe la prima riga presente dentro la tabella e, in questo caso, stamperebbe il primo utente della tabella che in generale è sempre l’amministratore! Generalmente in una pagina non viene mai mostrata la password potreste pensare, ma se la pagina servisse a recuperarla? A seconda di come è stata progettata potrebbe recapitarci sulla nostra mail senza fatiche dati sensibili.

3) Delimitatore di commento

Questo metodo è stato uno degli ultimi in ordine cronologico che ho scoperto e, mano a mano che vengo a conoscenza di altri, mi rendo conto che davvero non c’è limite alla fantasia di chi vuole arrecarti un danno. Dalle vecchie nozioni di MySQL sappiamo che la seguente sequenza di caratteri “– ” (spazio incluso, senza apici) è il delimitatore di commenti all’interno del db. Supponiamo avere una form di login e una pagina che elabora i dati eseguendo una query di questo genere:

SELECT * FROM 'users' WHERE user='pippo' AND password='pluto';

Se l’utente inserisse nel campo user della form una stringa di questo genere (senza apici e con spazio finale): “qualsiasi_utente’ OR 1=1 — ” la query diventerebbe di questo genere:

SELECT * FROM 'users' WHERE user='qualsiasi_utente' OR 1=1 -- ' AND password='qualsiasi';

Il database interpreterebbe solo la parte che ho lasciata colorata mentre ignorerebbe la parte di query colorata di grigio. La query risulterebbe sempre vera perchè c’è la clausola 1=1 restituendo la prima riga che trova all’interno della tabella facendoci loggare come utente registrato senza problemi!

4) Utilizzo costrutto UNION

Supponiamo di avere una query di questo genere:

SELECT id_documento, titolo FROM 'articoli' WHERE id_autore=10;

Si potrebbe usare il costrutto UNION per falsare il risultato e mostrare a schermo ben altro rispetto a quello che si sarebbe aspettato il programmatore in fase di progettazione. Usando quindi una UNION la query potrebbe diventare:

SELECT id_documento, titolo FROM 'articoli' WHERE id_autore=10 AND 0
UNION SELECT user, password FROM users;

Sel’applicazione prevede un output per tali dati (troveremo i valori di user che corrisponderanno alla colonna id_documento, e passwordd a titolo), il cracker ha la lista completa di login e password degli utenti. Vengono estratti solamente i dati soddisfacenti alla seconda SELECT, in quanto la condizione AND 0 rende sempre falsa la prima.

Ciò che il cracker vede è la lista del contenuto della base dati, formattato secondo quanto definito dal progettista dell’applicazione. Ad esempio: la lista di nomi utente affiancati a pulsanti “Aggiungi al carrello”, se si tratta di acquisti online. Ovviamente poco importa la “strana” rappresentazione grafica risultante, ma il contenuto.

Come abbiamo potuto notare quindi le possibilità di arrecare danno alle nostre applicazioni sono davvero svariate. Ho messo solo 4 esempi ma come ho già detto con un pò di fantasia utilizzando questi esempi si potrebbero avere effetti decisamente spiacevoli per il nostro database. Prima di chiudere quindi voglio riassumere le tecniche che possiamo utilizzare (anche combinate tra di loro) per prevenire attacchi di tipo SQL-Injection:

  • controllare il tipo dei dati ricevuti (se ad esempio ci si aspetta un valore numerico, controllare che l’input sia un valore numerico);
  • forzare il tipo dei dati ricevuti (se ad esempio ci si aspetta un valore numerico, si può forzare l’input affinché diventi comunque un valore numerico);
  • filtrare i dati ricevuti attraverso le espressioni regolari (regex);
  • sostituire i caratteri pericolosi con equivalenti caratteri innocui (ad esempio in entità html, oppure utilizzando le funzioni addslashes e stripslashes di PHP);
  • effettuare l’escape dei dati ricevuti (ogni linguaggio, solitamente, mette a disposizione particolari funzioni per questo scopo).
  • nel caso del login, criptare le credenziali di accesso prima di inserirle nella query SQL (evitare che le informazioni sensibili siano memorizzate nel DB in chiaro).
Wikipedia

Aggiungerei alle tecniche sopra citate queste tecniche:

  • Impostare la direttiva magic_quotes_gpc del php.ini ad ON. Tale direttiva equivale ad un addslashes automatico sui caratteri considerati pericolosi relativi a tutte le stringhe passate via GET e POST e su tutto quanto salvato nei cookies.
  • Usare mysql_escape_string() oppure mysql_real_escape_string sulle variabili passate alle interrogazioni SQL.

Probabilmente continuerò a parlare di sicurezza nelle prossime settimane ma nel frattempo se ci sono critche o aggiunte all’articolo sono, come sempre, ben accette! :)

Articolo letto 500 volte.

Post correlati:

Inserisci un commento