SQL Injection: tecniche di sicurezza nei siti web
Introduzione
La sicurezza, in un' applicazione, è tutto. In questo tutorial andremo ad analizzare una tipologia di attacco molto diffusa e che, ben sfruttata, può causare danni ingenti e irreparabili: le SQL Injection. Ne vedremo le varie tipologie, dove ricercarle, come evitarle.
Cos'è un SQL Injection?
Con il termine SQL Injection si intende una particolare tecnica di attacco verso un' applicazione web, che consiste nel riuscire ad inserire veri e propri codici SQL da eseguire sul database. Tutto inizia nella form: se non vengono effettuati i necessari controlli su ciò che l' utente inserisce ed invia al server, si cade nella vulnerabilità. Una volta scoperto, l' attacker potrà inserirvi ogni tipologia di query SQL, da quella che elimina completamente le tabelle del database, a quella che ne legge alcuni campi.
Le SQL Injection si suddividono in due categorie: la prima si ha quando l' attacker, eseguita la query, ottiene immediatamente i dati richiesti; si ha invece il secondo tipo di SQL Injection quando l' attacker inietta nel database qualcosa, senza che esso gli restituisca risultati istantanei. Lo analizzeremo più approfonditamente in seguito.
Un esempio di attacco:
Consideriamo come esempio quello di un attacker che voglia, leggere alcuni records del database del sito vulnerabile. La pagina attaccata è quella dello Search interno al portale; se l' applicazione non farà i controlli opportuni, invierà al server qualcosa come:
string sql = "SELECT ProductName, QuantityPerUnit, UnitPrice " +
"FROM Products " +
"WHERE ProductName LIKE '" +this.search.Text+ "%';
SqlDataAdapter da = new SqlDataAdapter(sql, DbCommand);
da.Fill(productDataSet);
Il che non sarebbe sbagliato, ma non tiene conto di un importante fattore: e se il contenuto di this.search.Text non fosse un' innoqua stringa, e fosse invece una query?
Essì, perchè se l' utente immettesse, nel campo di ricerca, il codice:
' UNION SELECT name, type, id FROM sysobjects;--
sarebbero dolori. L' apostrofo (') iniziale infatti apre la via all' esecuzione di codice SQL, mentre i -- finali fanno capire al server che tutto ciò che potrà venire inserito, di default, nella query venga ignorato.
Fatto ciò, la pagina che normalmente dovrebbe contenere i risultati della ricerca effettuata verrà caricata, ma con una sorpresina: anzichè i risultati, uno sotto l' altro, verranno elencati tutti i nomi di tutti gli elementi presenti del database, completi di descrizione. L' attacker potrà così scoprire se, tra le tabelle, è presente una chiamata Users. Se esiste, la cosa potrebbe farsi molto interessante per lui, ed assai spiacevole per i webmasters e gli iscritti al sito. Supponendo che la tabella esista, l' attacker potrà prenderne l' ID (nell' esempio 1845581613) ed iniettare, com fatto in precedenza, una seconda query, più mirata:
' UNION SELECT name, '', length FROM syscolumns
WHERE id = 1845581613;--
Questo caricherà la pagina dei risultati, contenente l' elenco di tutte le colonne presenti nella tabella Users. A questo punto potrà mirare dritto agli usernames ed alle rispettive passwords degli iscritti, con una query del tipo:
' UNION SELECT UserName, Password, IsAdmin FROM Users;--
L' importanza di prestare sempre attenzione alla sicurezza:
Bisogna prestare grande attenzione alle pagine che richiedano l' interazione dell' utente con il server; nel caso in cui, infatti, l' utente risulti essere un attacker, potrebbe cercare di sfruttarla per penetrare nel server o eseguirvi alcune operazioni "a distanza".
Ma pure le applicazioni cosiddette intranet, ovvero che richiedono autenticazione tramite account Windows, e quindi limitate agli utenti di una rete locale, devono rispettare tutte le misure di sicurezza, dato che diverse statistiche affermano che una buona percentuale di attacchi proviene dall' interno.
Criptare i dati
Partendo dal presupposto che è sempre possibile che un
attacker penetri nel nostro sistema, può accadere,
è importante rendergli la vita difficile anche dopo
che vi ha guadagnato accesso. Come? Criptando i dati
più importanti, le passwords, ad esempio. Uno dei
metodi più diffusi per criptare passwords è
quello generare valori casuali da unire alla password
inserita dall' utente e criptare l' intera stringa,
grazie alla classe, del .NET Framework,FormsAuthentication (HashPasswordForStoringInConfigFile
method). Perchè non criptare direttamente
la password, ed aggiungervi invece valori casuali?
Perchè esistone una tipologia di attacco, chiamato
dictionary attack (attacco via duzionario), che estrae
singole parole da database immensi (dizionari), le cripta, e
confronta il risultato della stringa criptata con la password
criptata; se combaciano, la password è stata
trovata.L' aggiungere caratteri casuali alla password
secelta dall' utente fa in modo che la stringa criptata
non possa in alcun modo essere presente in un dizionario.
Meno privilegi - Account del database
Eseguire applicazioni che si connettano ad un database usando l' account administrator (cioè con massimi privilegi) spalanca la porta agli attackers, che, scovata l' SQL Injection, potranno eseguire ogni tipologia di query sul database.
Un esempio di attacco portato a segno con massimi privilegi può essere quello di un attacker che voglia scoprire il contenuto dell' hard disk del server. Innanzitutto, eseguirà questa query:
'; CREATE TABLE haxor(name varchar(255), mb_free int);
INSERT INTO haxor EXEC master..xp_fixeddrives;--
Ciò crea una nuova tabella che andrà a contenere i risultati dell' attacco, portato avanti nelle query sucessive.
In seguito, inietterà:
' UNION SELECT name, cast((mb_free) as varchar(10)), 1.0 FROM haxor;--
per conoscere quanti hard disk sono presenti, e la loro capacità in megabytes. Ora l' attacker conosce le lettere associate ai dischi rigidi, ed una nuova query può scovare cosa contengono:
'; DROP TABLE haxor;CREATE TABLE haxor(line varchar(255) null);
INSERT INTO haxor EXEC master..xp_cmdshell 'dir /s c:';--
Con l' ultima query stampa sullo schermo i risultati:
' UNION SELECT line, '', 1.0 FROM haxor;--
Da questo esempio si può capire bene come sia
rischioso il dare ad un' applicazione i privilegi da
admin senza che essi siano strettamente necessari per il suo
funzionamento. E' utile sapere che il comando xp_cmdshell di norma
può essere utilizzato solo da un utente con privilegi
sysadmin, così come i comandi sa e
CREATE TABLE lo sono tanto dai sysadmin, quanto
dai db_dbowner, quanto dai db_dlladmin.
Meno privilegi - Account dei processi
Quando un' istanza del server SQL è intallata su un computer, viene creato un servizio che si avvia in background e che processa i comandi provenienti dall' applicazione alla quale è collegata. Di default quest' istanza viene eseguita con i privilegi di Local System Account, ma questo, nel caso un attacker riesca ad eseguire query dall' applicazione, rappresenta un grosso rischio. Per questo è molto importante limitare i privilegi dell' istanza SQL sul server.
Ripulire e convalidare l' input
Alcuni programmatori possono voler, di proposito, permettere l' inserimento, nei campi di input, del carattere apostrofo, ad esempio per permettere l' inserimento di nomi quali "O'Brian". Esempio:
string surname = this.surnameTb.Text.Replace("'" , "''");
string sql = "Update Users SET Surname='" +surname+ "' " +
"WHERE id=" +userID;
Ciò però spalanca le porte alle SQL Injections.
Altro esempio, una form che richieda all' utente di ordinare per data alcuni risultati. Ecco la query corretta:
string sql = "SELECT * FROM Orders WHERE DATEPART(YEAR, OrderDate) = " +
this.orderYearTb.Text);
Anche in questo caso è facile iniettarvi del codice, scrivendo nel campo input:
0; DELETE FROM Orders WHERE ID = 'competitor';--
Ecco perchè diventa importantissimo assicurarsi, tramite apposite procedure, che ciò che è stato inserito non faccia parte di una query, ma che sia realmente un numero. Ecco una routine apposta, in C#:
string stringValue = orderYearTb.Text;
Regex re = new Regex(@"D");
Match m = re.Match(someTextBox.Text);
if (m.Success)
{
// Non è un numero.
}
else
{
int intValue = int.Parse(stringValue);
if ((intValue < 1990) || (intValue > DateTime.Now.Year))
{
// E' corretta, processala.
}
}
Secondo tipo di attacchi
Con gli attacchi del secondo tipo vengono inseriti dati nel database che torneranno utili all' attacker in un momento futuro.
Si consideri, come esempio, un' applicazione che permetta agli utenti di definire alcuni criteri di ricerca. Definiti tali parametri, l' applicazione elimina dalle stringhe di input tutti gli apostrofi, chiudendo la strada a tutti i tentativi di attacco tramite la prima tipologia di SQL Injections. A questo punto, avviata la ricerca, i dati vengono estratti dal database ed usati per formare una seconda query, volta a creare risultati coerenti ai criteri di ricerca scelti.In questo caso è proprio questa seconda query ad essere vulnerabile. Inserendo questi criteri, ad esempio:
'; DELETE Orders;--
l' applicazione genererà un SQL del tipo:
INSERT Favourites (UserID, FriendlyName, Criteria)
VALUES(123, 'My Attack', ''';DELETE Orders;--')
che verrà eseguita senza problemi. Talvolta quando l' utente sceglie i criteri di ricerca, i dati possono essere ricavati dall' applicazione ed utilizzati per formare una nuova query; ecco un esempio:
// prende il valore dell' username e del friendly name scelto nei criteri
int uid = this.GetUserID();
string friendlyName = this.GetFriendlyName();
// crea la prima query SQL per ricavare i criteri di ricerca
string sql = string.Format("SELECT Criteria FROM Favourites " +
"WHERE UserID={0} AND FriendlyName='{1}'" ,
uid, friendlyName);
SqlCommand cmd = new SqlCommand(sql, this.Connection);
string criteria = cmd.ExecuteScalar();
// effettua la ricerca
sql = string.Format("SELECT * FROM Products WHERE ProductName = '{0}'" ,
criteria);
SqlDataAdapter da = new SqlDataAdapter(sql, this.Connection);
da.Fill(this.productDataSet);
La seconda query, nel database, risulterà:
SELECT * FROM Products WHERE ProductName = ''; DELETE Orders;--
e ciò causerà la perdita di tutti gli ordini.
Parameterized queries
I servers SQL, come altri database, supportano un concetto chiamato parameterised queries, che consiste in queies SQL che usano uno o più parametri inseriti di default. Prendiamo per esempio questo codice:
string cmdText=string.Format("SELECT * FROM Customers " +
"WHERE Country='{0}'" , countryName);
SqlCommand cmd = new SqlCommand(cmdText, conn);
Esso nel caso di una parameterised queries si trasformerà in:
string commandText = "SELECT * FROM Customers " +
"WHERE Country=@CountryName";
SqlCommand cmd = new SqlCommand(commandText, conn);
cmd.Parameters.Add("@CountryName" ,countryName);
Usare le Stored Procedures
Le Stored Procedures aggiungono una barriera in più contro le SQL Injections. Infatti se l' accesso ai dati del database SQL è permesso solo tramite stored procedures, non sarà necessario impostare i permessi relativi a ciascuna tabella. Possono essere scritte per canvalidare un input, per assicurarsi della sua integrità. Per esempio, si consideri un database contenente i dati dei registrati ad un' applicazione; conterrà pure username e password. Cos'è importante? Che l' attacker non riesca ad avere la lista delle passwords e neanche una sola di esse. Grazie ad una stored procedure si potrà fare in modo che il campo password venga processato, ma mai mostrato come risultato. Una stored procedure per autenticare e registrare un utente potrebbe essere:
-
RegisterUser -
VerifyCredentials -
ChangePassword
RegisterUsers prenderebbe l' username e la password come parametri, e ritornerebbe l' UserID relativo;
La VerifyCredentials verrà usata per loggare all' interno del sito un utente: nel caso gli username e password inseriti corrispondano a qualcuno, verrà restituito l' UserID relativo, sennò UserID arriverà vuoto.
ChangePassword prenderà l' username, la vecchia password e la nuova. Se UserID e password corrispondono, quest0 ultima verrà cambiata.
Come si può vedere, le passwords vengono costantemente richiamate, ma mai mostrate.
Stored Procedures Caveat
Come si è già detto, è importante convalidare i dati inseriti dagli utenti, controllarli approfonditamente. La convalida diviene doppiamente importante nel caso in cui la stored procedure fa uso del comando EXEC(qual_cosa).
Per esempio, se la stored procedure modifica il modello dei dati in un database creando tabelle, il codice si può scrivere così:
CREATE PROCEDURE dbo.CreateUserTable
@userName sysname
AS
EXEC('CREATE TABLE '+@userName+
' (column1 varchar(100), column2 varchar(100))');
GO
Ciò che è contenuto in @userName, così facendo, viene inserito nella query, dopo il comando CREATE. Un attacker potrebbe inettare del codice che imposti l' username come:
a(c1 int); SHUTDOWN WITH NOWAIT;--
causando lo stop immediato del server SQL, senza che esso termini la richiesta.
Importante diviene quindi controllare i dati inseriti, accertarsi che l' input legato all' username non contenga spazi, ad esempio.
Messaggi di errore
I messaggi di errore possono facilmente divenire un' arma a favore degli attackers, in quanto possono fornire loro diverse informazioni importanti sulla nostra applicazione e sui nostri database.E' quindi bene mostrare il meno possibile negli errori che si presentano durante l' esecuzione delle applicazioni, inserendo magari un link all' email del supporto tecnico, nel caso si vogliano ricevere informazioni più dettagliate. Ecco il codice per raggiungere questo obiettivo:
try
{
// Prova alcune operazioni nel db
}
catch(Exception e)
{
errorLabel.Text = string.Concat("Ci scusiamo, richiesta fallita. " ,
"Se il problema persiste, vogliate segnalarlo " ,
"al supporto tecnico" , Environment.Newline, e.Message);
}
Alta possibilità, mostrare un errore standard, in questo modo:
try
{
// Prova alcune operazioni nel db
}
catch(Exception e)
{
int id = ErrorLogger.LogException(e);
errorLabel.Text = string.Format("Ci scusiamo, richiesta fallita. " +
"Se il problema persiste, vogliate segnalare il codice errore {0} "
"al supporto tecnico." , id);
}
- Articolo precedente Ricerca all'interno di un file di testo
- Articolo successivo Transazioni sui database con ADO.NET
Ti potrebbe interessare anche
commenta la notizia
Chiedi alla nostra Redazione!