Spesso, i siti Web sembrano esistere principalmente per inserire qualcosa in un database al fine di estrarlo più tardi. Mentre altri metodi di database, come NoSQL, hanno guadagnato popolarità negli ultimi anni, i dati per molti siti Web risiedono ancora nel tradizionale database SQL. Questi dati spesso consistono in preziose informazioni personali come numeri di carte di credito e altre informazioni personali di interesse per ladri e criminali di identità. Gli hacker quindi cercano sempre di ottenere questi dati. Uno dei bersagli più comuni di questi attacchi è il database SQL che si trova dietro molte applicazioni Web attraverso un processo di SQL Injection.
Un attacco di iniezione funziona facendo in modo che l'applicazione trasmetta l'input non attendibile all'interprete. Recentemente, 40.000 record di clienti presi da Bell Canada sono il risultato di un attacco SQL Injection. Alla fine del 2013, gli hacker hanno rubato oltre $ 100.000 da un ISP californiano utilizzando l'iniezione SQL.
L'Open Web Application Security Project (OWASP) ha scelto l'attacco di iniezione come il primo rischio per la sicurezza delle applicazioni nella top ten 2013, in base alla prevalenza e al rischio per i sistemi Web attaccati. Sfortunatamente, ha anche detenuto la posizione numero uno nel rapporto precedente del 2010. Che cos'è un attacco SQL Injection? In questo tutorial illustrerò come funzionano e cosa puoi fare per proteggere la tua applicazione da questi attacchi.
Qualsiasi sistema interpretato dietro un server Web può essere l'obiettivo di un attacco di iniezione. Gli obiettivi più comuni sono i server di database SQL dietro a molti siti Web. L'iniezione SQL non deriva direttamente dai punti deboli del database, ma utilizza le aperture nell'applicazione per consentire all'utente malintenzionato di eseguire dichiarazioni di scelta dell'hacker sul server. Un attacco di SQL injection consente al database di condividere più informazioni di quante l'applicazione è progettata per fornire. Diamo un'occhiata a una chiamata al database SQL che potresti scrivere in ASP.NET.
Comando SqlCommand = new SqlCommand ("SELECT * FROM userdata WHERE UserId =" + id); SqlDataReader reader = command.ExecuteReader ();
Cosa c'è di sbagliato con questo codice? Forse niente. Il problema è questo id
stringa che stiamo usando. Da dove viene quel valore? Se lo stiamo generando internamente o da una fonte attendibile, allora questo codice potrebbe funzionare senza problemi. Tuttavia, se otteniamo il valore da un utente e utilizziamo senza modifiche, ci siamo appena aperti all'iniezione SQL.
Prendiamo un caso comune in cui stiamo ottenendo il parametro per cercare come parte dell'URL. Prendi l'URL http://www.example.com/user/details?id=123
. In ASP.NET utilizzando C # possiamo ottenere quel valore passato usando questo codice:
string id = Request.QueryString ["id"];
Questo codice combinato con la chiamata sopra ci lascia aperti ad un attacco. Se l'utente passa un id utente come 123 come previsto, tutto funziona correttamente. Tuttavia, nulla di ciò che abbiamo fatto assicura che questo sia il caso. Supponiamo che l'utente malintenzionato tenti di accedere all'URL http://www.example.com/user/details?id=0; SELECT * FROM userdata
.
Invece di passare semplicemente un valore come previsto, abbiamo dato un valore e poi aggiunto un punto e virgola, che termina un'istruzione SQL. Quindi aggiunge una seconda istruzione SQL che l'utente malintenzionato vorrebbe eseguire. In questo caso restituirebbe tutti i record nella tabella userdata. A seconda del resto del codice nella nostra pagina, potrebbe restituire un errore o forse visualizzare ogni record nel database per l'aggressore. Anche un errore può essere utilizzato con query costruite con cura per costruire una vista del database. Peggio ancora, immagina che l'aggressore vada a un URL di http://www.example.com/user/details?id=0; DROP TABLE userdata
. Ora tutti i tuoi utenti sono persi.
In questo esempio, l'utente malintenzionato può eseguire qualsiasi codice che desidera sul server di database. Se l'account in cui vengono eseguite le chiamate al database ha il controllo completo del database, uno scenario troppo comune, quindi eliminare le tabelle ed eliminare record rende un modo semplice per far cadere un sito. Anche se l'utente malintenzionato può solo leggere e scrivere dati nel database, quindi puling dati è solo una questione di pazienza e attenzione.
Prendi un semplice database chiamato "Prodotti" composto da tre colonne. La prima colonna contiene l'ID del prodotto, la seconda contiene il nome del prodotto e la terza contiene il prezzo del prodotto. Per la nostra normale query cercheremo di trovare ogni prodotto contenente widget nel nome. L'SQL per fare questo nello stesso modello che abbiamo mostrato sarebbe simile a questo:
string sql = "SELECT * FROM Prodotti WHERE productname LIKE '%" + searchterm + "%'";
Un sito web tipico per accedere a questo sarebbe simile http://www.example.com/product?search=widget
. Qui la pagina web scorre semplicemente ogni record restituito e lo visualizza sullo schermo. In questo caso vediamo il nostro prodotto widget.
| productid | nomeprodotto | prezzo | | ----------- | 1 | Widget | 100,00 |
Cambiamo la nostra richiesta a http://www.example.com/product?search=widget 'OR 1 = 1;--
ed eseguire la stessa query. vedere qualcosa di più. In effetti vedremo ogni record nella tabella.
| productid | nome prodotto | prezzo | | --- | 1 | Widget | 100,00 | | 2 | Thingy | 50,00 | | 3 | Boxy | 125,00 |
Abbiamo ingannato l'interprete nell'esecuzione del codice SQL di nostra scelta. L'SQL risultante eseguito sarà:
SELECT * FROM Prodotti WHERE productname LIKE '% widget' OR 1 = 1; -% '
Il risultato saranno due dichiarazioni:
SELECT * FROM Prodotti WHERE productname LIKE '% widget' OR 1 = 1; -%'
Aggiungendo il 1 = 1
, che è sempre vero, la clausola where sarà vera per ogni riga della tabella e la query risultante restituirà ogni riga. Il --
all'inizio della seconda istruzione trasforma il resto dell'istruzione SQL in un commento che impedisce il messaggio di errore che altrimenti avremmo visto.
Le modifiche al display non possono impedire questa affermazione. Un utente malintenzionato può utilizzare anche solo il fatto che un valore viene restituito o meno e le query costruite con cura per mappare lentamente il database e recuperare i dati anche se visualizzano solo un messaggio di errore. Ci sono strumenti come sqlmap per automatizzare il processo.
Previene l'SQL injection evitando che input non attendibili arrivino al database SQL o ad altri interpreti. Qualsiasi input proveniente dall'esterno del sistema dovrebbe essere considerato non attendibile. Anche i dati provenienti da altri sistemi partner devono essere considerati non attendibili in quanto non si ha modo di garantire che l'altro sistema non soffra di problemi di sicurezza che consentirebbero l'inserimento di dati arbitrari e quindi trasmessi all'applicazione.
Tornando all'esempio precedente, se sappiamo che il parametro id deve sempre essere un numero intero, possiamo tentare di convertirlo in un numero intero e mostrare un errore se questo non riesce. Un'applicazione ASP.NET MVC farebbe questo usando un codice simile a questo:
quantità int; if (! int.TryParse (Request.QueryString ["qty"], out quantity)) return RedirectToAction ("Invalid");
Questo tenterà di convertire la stringa in un numero intero. Se la conversione fallisce, il codice reindirizza a un'azione che visualizzerà un messaggio non valido.
Questo può essere evitato se stiamo cercando un parametro intero. Non sarebbe d'aiuto se ci aspettiamo un testo come nella ricerca di prodotti precedente. La tecnica preferita in questo caso è utilizzare espressioni regolari o sostituzioni di stringhe per consentire solo i caratteri necessari nel valore passato.
Questo può essere fatto con la whitelist, il processo di rimozione di qualsiasi carattere diverso da un set specificato o blacklisting, il processo di rimozione di un membro di un set specificato dalla stringa. La white list è più affidabile poiché si specificano solo i caratteri consentiti. Per rimuovere qualcosa di diverso da lettere e numeri in una stringa, possiamo usare codice come:
Regex regEx = new Regex ("[^ a-zA-Z0-9 -]"); string filteredString = regEx (originalString, "");
Dovrai valutare ogni input rispettivamente. Alcune query di database potrebbero richiedere maggiore attenzione specialistica. Assumere caratteri che possono essere significativi in un comando SQL ma potrebbe anche essere un carattere valido in una chiamata al database. Ad esempio il carattere di virgoletta singola 'è usato per iniziare e terminare una stringa in SQL, ma potrebbe anche far parte del nome di una persona come O'Conner. In questo caso sostituendo la quota singola '
con singole virgolette consecutive "
può eliminare il problema.
Le stored procedure sono spesso viste come la soluzione a questo problema e possono essere parte della soluzione. Tuttavia una procedura memorizzata male scritta non ti salverà. Prendi questa stored procedure che include codice simile a quello che abbiamo già visto per creare una query:
ALTER PROCEDURE [dbo]. [SearchProducts] @searchterm VARCHAR (50) = "AS BEGIN DECLARE @query VARCHAR (100) SET @query = 'SELECT * DA prodotti WHERE productname LIKE"%' + @searchterm + '% "; EXEC (@query) FINE
La concatenazione delle stringhe qui è il problema. Proviamo un semplice tentativo in cui tentiamo di impostare una condizione sempre true per mostrare tutte le righe nella tabella e passare nello stesso 'widget' OR 1 = 1; - "come la query che abbiamo visto in precedenza. Il risultato è anche lo stesso :
| productid | nome prodotto | prezzo | | - | 1 | Widget | 100,00 | | 2 | Thingy | 50,00 | | 3 | Boxy | 125,00 |
Se i dati non fidati vengono trasmessi, abbiamo lo stesso risultato come se la chiamata fosse stata creata nel nostro codice. Che la concatenazione di stringhe avvenga all'interno di una procedura memorizzata invece che nel nostro codice non fornisce alcuna protezione.
Il prossimo pezzo del puzzle per proteggere dagli attacchi per iniezione arriva usando la parametrizzazione. La creazione di query SQL concatenando stringhe e quindi passando il codice finito non fornisce al database alcuna idea di quale parte della stringa sia un parametro e che cosa fa parte del comando. Possiamo aiutare a proteggere dagli attacchi creando chiamate SQL in un modo che mantenga distinte le dichiarazioni e i valori.
Possiamo riscrivere la stored procedure mostrata in precedenza per utilizzare i parametri e produrre una chiamata più sicura. Invece di concatenare il %
i caratteri che rappresentano i caratteri jolly, creeremo una nuova stringa aggiungendo questi caratteri e quindi passeremo questa nuova stringa come parametro all'istruzione SQL. La nuova stored procedure ha il seguente aspetto:
ALTER PROCEDURE [dbo]. [SearchProductsFixed] @searchterm NVARCHAR (50) = "AS BEGIN DECLARE @query NVARCHAR (100) DECLARE @msearch NVARCHAR (55) SET @msearch = '%' + @searchterm + '%' SET @query = 'SELECT * FROM Prodotti WHERE productname LIKE @search' EXEC sp_executesql @query, N '@ cerca VARCHAR (55)', @msearch END
L'esecuzione di questa stored procedure con il solo widget di parole funziona come previsto.
| productid | nome prodotto | prezzo | ---- | 1 | Widget | 100,00
E se passiamo con il nostro widget parametro "OR 1 = 1; - non viene restituito nulla che mostri che non siamo più vulnerabili a questo attacco.
La parametrizzazione non richiede procedure memorizzate. Puoi anche approfittarne con le query create all'interno del codice. Ecco un breve segmento in C # per connettersi ed eseguire una query sul server Microsoft SQL utilizzando un parametro.
const string sql = "SELECT * FROM Prodotti WHERE productname LIKE @CategoryID"; var connString = WebConfigurationManager.ConnectionStrings ["ProductDatabase"]. ConnectionString; using (var conn = new SqlConnection (connString)) var command = new SqlCommand (sql, conn); command.Parameters.Add ("@ searchterm", SqlDbType.NVarChar) .Value = string.Format ("% 0%", searchTerm); command.Connection.Open (); SqlDataReader reader = command.ExecuteReader (); while (reader.NextResult ()) // Il codice da eseguire su ogni riga va qui
Fino a questo punto, ho mostrato come mitigare gli attacchi ai database. Un altro importante livello di difesa, minimizza le cause del danno nel caso in cui l'attaccante superi le altre difese. Il concetto di privilegio minimo prevede che un modulo di codice che, in questo caso chiama il nostro database, dovrebbe avere accesso solo alle informazioni e alle risorse necessarie ai suoi scopi.
Quando viene eseguito un comando di database, lo fa con i diritti di un account utente. Otteniamo sicurezza dando all'account le chiamate al database eseguite solo con i diritti per fare le cose che normalmente deve fare. Se le chiamate da un database devono solo leggere i dati da una tabella, fornire solo i diritti di selezione dell'account alla tabella e non inserire o eliminare. Se ci sono tabelle specifiche è necessario aggiornare, ad esempio una tabella di ordini, quindi dargli inserire e aggiornare a quella tabella, ma non altre tabelle come quella contenente le informazioni utente.
La cancellazione degli ordini può essere gestita tramite un account separato utilizzato solo nella pagina che esegue tale attività. Ciò impedirebbe la cancellazione di un ordine dalla tabella altrove più difficile. Utilizzando un account di database separato per le funzioni amministrative del sito, con i diritti maggiori necessari rispetto a quello che l'uso pubblico può fare molto per prevenire danni da un utente che trova un attacco di iniezione aperto.
Questo non impedirà tutti gli attacchi. Non farebbe nulla per evitare di restituire risultati extra come l'esempio precedente che mostra l'intero contenuto di una tabella. Eviterebbe gli attacchi dall'aggiornamento o dall'eliminazione dei dati.
L'iniezione SQL è l'attacco più pericoloso, soprattutto se si tiene conto di quanto sono vulnerabili i siti Web e di quanto potenziale questo tipo di attacco possa causare un sacco di danni. Qui ho descritto gli attacchi SQL injection e ho dimostrato il danno che si può fare. Fortunatamente non è così difficile proteggere i tuoi progetti web da questa vulnerabilità seguendo alcune semplici regole.
Non fidarti mai di dati esterni introdotti nella tua applicazione. Dovrebbe essere convalidato rispetto a una whitelist di input validi prima di essere elaborati ulteriormente. Ciò può significare garantire che un parametro intero sia effettivamente un numero intero o che una data sia un valore di data valido. Convalidare anche il testo per includere solo i caratteri di cui il parametro avrebbe bisogno. Per una ricerca di testo puoi spesso consentire solo lettere e numeri e filtrare la punteggiatura che potrebbe essere problematica come il segno di uguale o un punto e virgola.
Utilizzare la parametrizzazione ed evitare la concatenazione di stringhe durante la creazione di chiamate SQL. Le stored procedure non sono una panacea in quanto possono essere vulnerabili anche se viene utilizzata una semplice concatenazione di stringhe. La parametrizzazione evita molti dei problemi di concatenazione delle stringhe.
Il codice che accede al database deve essere eseguito con i privilegi minimi necessari per completare le attività necessarie. In alcune circostanze, le chiamate al database utilizzate dall'applicazione Web devono apportare modifiche alla struttura del database, ad esempio l'eliminazione o la modifica delle tabelle. È possibile aggiungere un ulteriore livello di protezione eseguendo parti separate del sito Web in diversi account. L'account del database utilizzato per le normali azioni dell'utente probabilmente non ha motivo di modificare la tabella contenente i ruoli o i diritti degli utenti. Esecuzione delle parti amministrative del sito con un account più privilegiato e la sezione utente finale con una meno privilegiata può fare molto per attenuare le possibilità che il codice sfugga causando altri problemi.