Perché il meccanismo di prevenzione dell'iniezione SQL si è evoluto nella direzione dell'utilizzo di query parametrizzate?

59

Per come la vedo io, gli attacchi di SQL injection possono essere prevenuti con:

  1. Filtraggio attento, filtro, input di codifica (prima dell'inserimento in SQL)
  2. Uso delle istruzioni preparate / query parametrizzate

Suppongo che ci siano pro e contro per ciascuno, ma perché il # 2 è decollato e considerato più o meno il modo di fatto di prevenire attacchi di iniezione? È solo più sicuro e meno incline all'errore o ci sono altri fattori?

Come ho capito, se il # 1 è usato correttamente e tutti gli avvertimenti sono presi in considerazione, può essere altrettanto efficace del # 2.

Sanitizing, Filtering e Encoding

C'è stata una certa confusione da parte mia tra ciò che significa sanitizing , filtering e encoding . Dirò che per i miei scopi, tutto quanto sopra può essere considerato per l'opzione 1. In questo caso capisco che la disinfezione e il filtraggio hanno il potenziale di modificare o scartare i dati di input, mentre la codifica conserva i dati così com'è , ma lo codifica correttamente per evitare attacchi di iniezione. Credo che l'escaping dei dati possa essere considerato un modo per codificarlo.

Query parametrizzate e libreria di codifica

Ci sono risposte in cui i concetti di parameterized queries e encoding libraries sono trattati in modo intercambiabile. Correggimi se sbaglio, ma ho l'impressione che siano diversi.

La mia comprensione è che encoding libraries , non importa quanto siano bravi, hanno sempre il potenziale per modificare il "Programma" SQL, perché stanno facendo delle modifiche allo stesso SQL, prima che venga inviato al RDBMS.

Parameterized queries d'altra parte, invia il programma SQL all'RDBMS, che quindi ottimizza la query, definisce il piano di esecuzione della query, seleziona gli indici che devono essere utilizzati, ecc., e quindi inserisce i dati, come l'ultimo passaggio all'interno dello stesso RDBMS.

Libreria di codifica

  data -> (encoding library)
                  |
                  v
SQL -> (SQL + encoded data) -> RDBMS (execution plan defined) -> execute statement

Query parametrizzata

                                               data
                                                 |
                                                 v
SQL -> RDBMS (query execution plan defined) -> data -> execute statement

Rilevanza storica

Alcune risposte menzionano che storicamente, le query parametriche (PQ) sono state create per motivi di prestazioni e prima che gli attacchi di iniezione che miravano a problemi di codifica diventavano popolari. Ad un certo punto è apparso evidente che i PQ erano anche piuttosto efficaci contro gli attacchi di iniezione. Per mantenere lo spirito della mia domanda, perché PQ è rimasto il metodo di scelta e perché è fiorito sopra la maggior parte degli altri metodi quando si tratta di prevenire attacchi di SQL injection?

    
posta Dennis 12.09.2016 - 16:04
fonte

14 risposte

146

Il problema è che # 1 richiede di analizzare e interpretare in modo efficace l'intera variante SQL a cui stai lavorando, in modo da sapere se sta facendo qualcosa che non dovrebbe. E mantieni aggiornato il codice man mano che aggiorni il tuo database. Ovunque accetti l'input per le tue query. E non rovinare.

Quindi sì, questo genere di cose fermerebbe gli attacchi SQL injection, ma è assurdamente più costoso da implementare.

    
risposta data 12.09.2016 - 16:08
fonte
79

Perché l'opzione 1 non è una soluzione. Screening e filtraggio significa rifiutare o rimuovere input non validi. Ma qualsiasi input potrebbe essere valido. Ad esempio l'apostrofo è un personaggio valido nel nome "O'Malley". Deve solo essere codificato correttamente prima di essere utilizzato in SQL, che è ciò che le istruzioni preparate fanno.

Dopo aver aggiunto la nota, sembra che tu stia fondamentalmente chiedendo perché utilizzare una funzione di libreria standard piuttosto che scrivere il tuo codice funzionalmente simile da zero? Devi sempre preferire soluzioni di libreria standard per scrivere il tuo codice. È meno lavoro e più mantenibile. Questo è il caso della qualsiasi funzionalità, ma soprattutto per qualcosa che è sensibile alla sicurezza non ha assolutamente senso reinventare la ruota da solo.

    
risposta data 12.09.2016 - 18:07
fonte
60

Se si sta tentando di eseguire l'elaborazione delle stringhe, in realtà non si sta generando una query SQL. Stai generando una stringa che può produrre una query SQL. C'è un livello di riferimento indiretto che apre un lotto di spazio per errori e bug. È davvero sorprendente, dato che nella maggior parte dei contesti siamo felici di interagire con qualcosa a livello di programmazione. Ad esempio, se abbiamo una struttura delle liste e vogliamo aggiungere un elemento, di solito non lo facciamo:

List<Integer> list = /* a list of 1, 2, 3 */
String strList = list.toString();   /* to get "[1, 2, 3]" */
strList = /* manipulate strList to become "[1, 2, 5, 3]" */
list = parseList(strList);

Se qualcuno suggerisce di farlo, rispondi giustamente che è piuttosto ridicolo e che dovresti semplicemente fare:

List<Integer> list = /* ... */;
list.add(5, position=2);

Questo interagisce con la struttura dei dati a livello concettuale. Non introduce alcuna dipendenza su come la struttura potrebbe essere stampata o analizzata. Quelle sono decisioni completamente ortogonali.

Il tuo primo approccio è come il primo esempio (solo un po 'peggio): stai assumendo che sia possibile costruire in modo programmatico la stringa che verrà analizzata correttamente come query desiderata. Ciò dipende dal parser e da una serie di logiche di elaborazione delle stringhe.

Il secondo approccio dell'utilizzo di query preparate è molto più simile al secondo campione. Quando si utilizza una query preparata, si esegue essenzialmente l'analisi di una pseudo-query che è legale ma contiene alcuni segnaposti e quindi si utilizza un'API per sostituire correttamente alcuni valori. Non coinvolgi più il processo di analisi e non devi preoccuparti di alcuna elaborazione delle stringhe.

In generale, è molto più facile, e molto meno incline agli errori, interagire con le cose a livello concettuale. Una query non è una stringa, una query è ciò che ottieni quando analizzi una stringa o la costruisci a livello di programmazione (o qualsiasi altro metodo ti consenta di crearne una).

Qui c'è una buona analogia tra le macro in stile C che eseguono semplici rimpiazzi di testo e macro in stile Lisp che generano codice arbitrario. Con le macro in stile C, puoi sostituire il testo nel codice sorgente e ciò significa che hai la possibilità di introdurre errori sintattici o comportamenti fuorvianti. Con le macro Lisp, stai generando codice nella forma che il compilatore elabora (ovvero, stai restituendo le strutture dati effettive che il compilatore elabora, non il testo che il lettore deve elaborare prima che il compilatore possa raggiungerlo) . Con una macro Lisp, però, non è possibile generare qualcosa che sarebbe un errore di analisi. Ad esempio, non puoi generare (let ((a b) a .

Anche con le macro Lisp, puoi comunque generare codice cattivo, perché non devi necessariamente essere consapevole della struttura che dovrebbe essere lì. Ad esempio, in Lisp, (let ((ab)) a) significa "stabilisce un nuovo legame lessicale della variabile a al valore della variabile b, e quindi restituisce il valore di a", e < strong> (let (ab) a) significa "stabilire nuovi collegamenti lessicali delle variabili aeb e inizializzarli entrambi a zero, e quindi restituire il valore di a." Quelli sono entrambi sintatticamente corretti, ma significano cose diverse. Per evitare questo problema, è possibile utilizzare più funzioni semanticamente consapevoli e fare qualcosa del tipo:

Variable a = new Variable("a");
Variable b = new Variable("b");
Let let = new Let();
let.getBindings().add(new LetBinding(a,b));
let.setBody(a);
return let;

Con qualcosa del genere, è impossibile restituire qualcosa che è sintatticamente non valido, ed è molto più difficile restituire qualcosa che accidentalmente non è quello che volevi.

    
risposta data 13.09.2016 - 00:06
fonte
21

Aiuta che l'opzione # 2 è generalmente considerata una best practice perché il database può memorizzare nella cache la versione non parametrizzata della query. Le query parametrizzate precedono il problema dell'iniezione SQL da diversi anni (credo), ma solo così puoi uccidere due piccioni con una fava.

    
risposta data 12.09.2016 - 19:29
fonte
20

Semplicemente detto: non l'hanno fatto. La tua affermazione:

Why did SQL Injection prevention mechanism evolve into the direction of using Parameterized Queries?

è fondamentalmente difettoso. Le query parametrizzate sono esistite molto più a lungo di quanto l'SQL Injection sia almeno ampiamente conosciuto. Sono stati generalmente sviluppati come un modo per evitare la concentazione delle stringhe nella solita "forma per la ricerca" delle applicazioni LOB (Line of Business). Molti - MOLTI - anni dopo, qualcuno ha trovato un problema di sicurezza con detta manipolazione delle stringhe.

Ricordo di aver fatto SQL 25 anni fa (quando Internet non era ampiamente utilizzato - era appena iniziato) e ricordo di aver fatto SQL vs. IBM DB5 IIRC versione 5 - e che aveva già query parametrizzate.

    
risposta data 14.09.2016 - 13:44
fonte
13

Oltre a tutte le altre buone risposte:

Il motivo per cui # 2 è migliore è perché separa i tuoi dati dal tuo codice. Nel n. 1 i tuoi dati fanno parte del tuo codice ed è da lì che provengono tutte le cose negative. Con # 1 si ottiene la query e occorre eseguire ulteriori passaggi per assicurarsi che la query comprenda i dati come dati, mentre in # 2 si ottiene il codice e il codice e i dati sono dati.

    
risposta data 13.09.2016 - 08:56
fonte
11

Le query parametrizzate, oltre a fornire la difesa di SQL injection, hanno spesso un ulteriore vantaggio di essere compilate una sola volta, quindi eseguite più volte con parametri diversi.

Dal punto di vista del database SQL select * from employees where last_name = 'Smith' e select * from employees where last_name = 'Fisher' sono nettamente differenti e richiedono quindi analisi, compilazione e ottimizzazione separati. Occuperanno anche slot separati nell'area di memoria dedicata alla memorizzazione di istruzioni compilate. In un sistema pesantemente caricato con un numero elevato di query simili che hanno parametri diversi, il calcolo e il sovraccarico della memoria possono essere notevoli.

Successivamente, l'utilizzo di query con parametri spesso offre importanti vantaggi in termini di prestazioni.

    
risposta data 12.09.2016 - 19:36
fonte
5

Aspetta, ma perché?

L'opzione 1 significa che devi scrivere routine di sanitizzazione per ogni tipo di input, mentre l'opzione 2 è meno soggetta a errori e meno codice da scrivere / testare / mantenere.

Quasi certamente "prendersi cura di tutti gli avvertimenti" può essere più complesso di quanto si pensi, e la tua lingua (ad esempio Java PreparedStatement) ha più problemi di quanto pensi.

Le istruzioni preparate o le query parametrizzate sono precompilate nel server di database, quindi, quando vengono impostati i parametri, non viene eseguita alcuna concatenazione SQL perché la query non è più una stringa SQL. Un vantaggio aggiuntivo è che RDBMS memorizza nella cache la query e le chiamate successive sono considerate come lo stesso SQL anche quando i valori dei parametri variano, mentre con SQL concatenato ogni volta che la query viene eseguita con valori diversi la query è diversa e RDBMS deve analizzarla , crea nuovamente il piano di esecuzione, ecc.

    
risposta data 12.09.2016 - 16:17
fonte
1

Immaginiamo quale sarebbe l'ideale "igienizzare, filtrare e codificare" l'approccio.

L'igienizzazione e il filtraggio potrebbero avere senso nel contesto di una particolare applicazione, ma alla fine entrambi si riducono a dire "non puoi mettere questi dati nel database". Per la tua applicazione, potrebbe essere una buona idea, ma non è qualcosa che puoi raccomandare come soluzione generale, dal momento che ci saranno applicazioni che devono essere in grado di memorizzare caratteri arbitrari nel database.

Quindi lascia la codifica. Si potrebbe iniziare con una funzione che codifica le stringhe aggiungendo caratteri di escape, in modo da poterli sostituire in se stessi. Poiché diversi database richiedono l'escape di caratteri diversi (in alcuni database sia \' che '' sono sequenze di escape valide per ' , ma non in altre), questa funzione deve essere fornita dal fornitore del database.

Ma non tutte le variabili sono stringhe. A volte è necessario sostituire un numero intero o una data. Questi sono rappresentati in modo diverso rispetto alle stringhe, quindi sono necessari diversi metodi di codifica (anche in questo caso dovrebbero essere specifici per il fornitore del database) ed è necessario sostituirli nella query in modi diversi.

Quindi forse le cose sarebbero più facili se il database gestisse la sostituzione anche per te - già sa quali tipi la query si aspetta, e come codificare i dati in modo sicuro, e come sostituirli nella tua query in modo sicuro, quindi non hai bisogno preoccuparti di questo nel tuo codice.

A questo punto, abbiamo appena reinventato le query con parametri.

Una volta parametrizzate, le query aprono nuove opportunità, come l'ottimizzazione delle prestazioni e il monitoraggio semplificato.

La codifica è difficile da eseguire correttamente e la codifica-done-right non è distinguibile dalla parametrizzazione.

Se ti piace davvero l'interpolazione delle stringhe come metodo per costruire le query, ci sono un paio di linguaggi (Scala ed ES2015 vengono in mente) che hanno un'interpolazione di stringa innestabile, quindi ci sono librerie che ti permettono di scrivere query parametrizzate che assomigliano all'interpolazione di stringhe, ma sono al sicuro dall'iniezione SQL - così nella sintassi ES2015:

import {sql} from 'cool-sql-library'

let result = sql'select *
    from users
    where user_id = ${user_id}
      and password_hash = ${password_hash}'.execute()

console.log(result)
    
risposta data 14.09.2016 - 18:08
fonte
0

Nell'opzione 1, stai lavorando con un set di input di size = infinity che stai cercando di mappare su una dimensione di output molto grande. Nell'opzione 2, hai limitato il tuo input a qualsiasi cosa tu scelga. In altre parole:

  1. Filtrare attentamente e filtrare [ infinito ] per [ tutte le query SQL sicure ]
  2. Utilizzo di [ scenari preconsiderati limitati al tuo ambito ]

Secondo altre risposte, ci sono anche alcuni vantaggi in termini di prestazioni dal limitare la portata dall'infinito e verso qualcosa di gestibile.

    
risposta data 12.09.2016 - 22:34
fonte
0

Un utile modello mentale di SQL (in particolare dialetti moderni) è che ogni istruzione o query SQL è un programma. In un programma eseguibile binario nativo, i tipi più pericolosi di vulnerabilità di sicurezza sono overflow in cui un utente malintenzionato può sovrascrivere o modificare il codice del programma con istruzioni diverse.

Una vulnerabilità di SQL injection è isomorfa a un overflow del buffer in un linguaggio come C. La cronologia ha dimostrato che i buffer overflow sono estremamente difficili da prevenire - anche il codice estremamente critico soggetto a revisione aperta ha spesso contenuto tali vulnerabilità.

Un aspetto importante dell'approccio moderno alla risoluzione delle vulnerabilità di overflow è l'uso di meccanismi hardware e OS per contrassegnare parti particolari della memoria come non eseguibili e per contrassegnare altre parti della memoria come di sola lettura. (Vedere l'articolo di Wikipedia su Protezione dello spazio eseguibile , ad esempio.) In questo modo, anche se un utente malintenzionato può modificare i dati, il l'autore dell'attacco non può causare il trattamento dei dati iniettati come codice.

Quindi, se una vulnerabilità di SQL injection equivale a un overflow del buffer, qual è l'equivalente SQL di un bit NX o di una memoria di sola lettura? La risposta è: istruzioni preparate , che includono query parametrizzate più meccanismi simili per richieste non di query. L'istruzione preparata viene compilata con alcune parti contrassegnate come di sola lettura, quindi un utente malintenzionato non può modificare quelle parti del programma e altre parti contrassegnate come dati non eseguibili (i parametri dell'istruzione preparata), che l'utente malintenzionato potrebbe inserire dati in ma che non sarà mai trattato come codice di programma, eliminando così la maggior parte del potenziale di abuso.

Certamente, l'igienizzazione dell'input dell'utente è buona, ma per essere veramente sicuri devi essere paranoico (o, equivalentemente, pensare come un aggressore). Una superficie di controllo al di fuori del testo del programma è il modo per farlo e le istruzioni preparate forniscono quella superficie di controllo per SQL. Quindi non dovrebbe sorprendere che le dichiarazioni preparate, e quindi le query parametrizzate, siano l'approccio consigliato dalla stragrande maggioranza dei professionisti della sicurezza.

    
risposta data 13.09.2016 - 08:23
fonte
0

Ne ho già parlato qui: link

Ma, per semplicità:

Il modo in cui le query parametrizzate funzionano, è che sqlQuery viene inviato come una query, e il database sa esattamente cosa farà questa query, e solo allora inserirà semplicemente username e password come valori. Ciò significa che non possono effettuare la query, poiché il database sa già cosa farà la query. Quindi in questo caso cercherebbe il nome utente "Nobody OR 1 = 1" - "e una password vuota, che dovrebbe apparire falsa.

Questa non è una soluzione completa, e la validazione dell'input dovrà ancora essere eseguita, poiché ciò non influirà su altri problemi, come gli attacchi XSS, poiché potresti comunque inserire javascript nel database. Quindi, se questo viene letto su una pagina, lo visualizzerà come javascript normale, a seconda della convalida dell'output. Quindi, la cosa migliore da fare è utilizzare la convalida dell'input, ma utilizzando query parametrizzate o stored procedure per interrompere qualsiasi attacco SQL

    
risposta data 16.09.2016 - 10:25
fonte
0

Non ho mai usato SQL. Ma ovviamente si sente parlare dei problemi che hanno le persone e gli sviluppatori SQL hanno avuto problemi con questa cosa di "SQL injection". Per molto tempo non sono riuscito a capirlo. E poi mi sono reso conto che le persone in cui creare istruzioni SQL, istruzioni di origine SQL testuali reali, concatenando stringhe, di cui alcune inserite da un utente. E il mio primo pensiero su quella realizzazione fu shock. Shock totale. Ho pensato: come può qualcuno essere così ridicolmente stupido e creare dichiarazioni in qualsiasi linguaggio di programmazione come quello? Per uno sviluppatore C, o C ++, o Java o Swift, questa è pazzia totale.

Detto questo, non è molto difficile scrivere una funzione C che prende come argomento una stringa C e produce una stringa diversa che assomiglia esattamente a una stringa letterale nel codice sorgente C che rappresenta la stessa stringa. Ad esempio, tale funzione tradurrebbe abc in "abc" e "abc" in "\" abc \ "" e "\" abc \ "" in "\" \\ "abc \\" \ "". (Beh, se questo ti sembra sbagliato, è html. Era giusto quando l'ho digitato, ma non quando viene visualizzato) E una volta che la funzione C è scritta, non è affatto difficile generare codice sorgente C dove il testo da un campo di input fornito dall'utente viene trasformato in un letterale di stringa C. Non è difficile da proteggere. Perché gli sviluppatori SQL non utilizzerebbero questo approccio come un modo per evitare le iniezioni SQL è oltre me.

"Sanitizing" è un approccio totalmente errato. Il difetto fatale è che rende alcuni input utente illegali. Si finisce con un database in cui un campo di testo generico non può contenere un testo simile; Drop Table o qualsiasi altra cosa che usereste in un'iniezione SQL per causare danni. Trovo che sia inaccettabile. Se un database memorizza il testo, dovrebbe essere in grado di memorizzare qualsiasi testo. E il difetto pratico è che il disinfettante non sembra aver capito bene: - (

Naturalmente, le query parametrizzate sono ciò che ci si aspetterebbe da qualsiasi programmatore che utilizza un linguaggio compilato. Rende la vita molto più semplice: hai un input per le stringhe, e non ti preoccupi nemmeno di tradurlo in una stringa SQL, ma basta passarlo come parametro, senza alcuna possibilità che i caratteri in quella stringa causino danni.

Quindi, dal punto di vista di uno sviluppatore che usa linguaggi compilati, l'igienizzazione è qualcosa che non mi verrebbe mai in mente. Il bisogno di sanificare è folle. Le query parametrizzate sono la soluzione ovvia al problema.

(Ho trovato interessante la risposta di Josip. Fondamentalmente dice che con query parametrizzate puoi fermare qualsiasi attacco contro SQL, ma poi puoi avere del testo nel tuo database che viene usato per creare un'iniezione JavaScript :-( Beh, abbiamo il lo stesso problema di nuovo, e non so se Javascript ha una soluzione a questo.

    
risposta data 15.05.2017 - 23:45
fonte
-2

Il problema principale è che gli hacker hanno trovato il modo di circondare le strutture igienico-sanitarie mentre le query parametrizzate erano una procedura esistente che funzionava perfettamente con i vantaggi extra delle prestazioni e della memoria.

Alcune persone semplificano il problema in quanto "è solo una singola citazione e una doppia citazione", ma gli hacker hanno trovato modi intelligenti per evitare il rilevamento come l'utilizzo di codifiche diverse o l'uso di funzioni di database.

Ad ogni modo, hai solo dovuto dimenticare una singola stringa per creare una violazione dei dati catastrofica. Hacker in grado di automatizzare gli script per scaricare il database completo con una serie o query. Se il software è ben noto come una suite open source o una famosa suite di business, potresti semplicemente attirare la tabella degli utenti e delle password.

D'altro canto, usare solo le query concatenate era solo questione di imparare a usarlo e abituarsi ad esso.

    
risposta data 14.09.2016 - 17:59
fonte

Leggi altre domande sui tag