Le istruzioni preparate per DOP sono sufficienti per prevenire l'iniezione di SQL?


660

Diciamo che ho un codice come questo:

$dbh = new PDO("blahblah");

$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

La documentazione DOP afferma:

I parametri per le istruzioni preparate non devono essere citati; l'autista lo gestisce per te.

È davvero tutto ciò che devo fare per evitare iniezioni di SQL? È davvero così facile?

Puoi assumere MySQL se fa la differenza. Inoltre, sono davvero solo curioso dell'uso di istruzioni preparate contro l'iniezione SQL. In questo contesto, non mi interessa XSS o altre possibili vulnerabilità.


5
approccio migliore settimo numero risposta stackoverflow.com/questions/134099/...
NullPoiиteя

Risposte:


807

La risposta breve è NO , i preparativi PDO non ti difenderanno da tutti i possibili attacchi di SQL Injection. Per alcuni casi oscuri.

Sto adattando questa risposta per parlare di DOP ...

La lunga risposta non è così semplice. È basato su un attacco dimostrato qui .

L'attacco

Quindi, iniziamo mostrando l'attacco ...

$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));

In determinate circostanze, ciò restituirà più di 1 riga. Analizziamo cosa sta succedendo qui:

  1. Selezione di un set di caratteri

    $pdo->query('SET NAMES gbk');

    Per questo tipo di attacco al lavoro, abbiamo bisogno della codifica che il server aspetta sulla connessione sia per codificare 'come in cioè ASCII 0x27 e di avere qualche personaggio il cui byte finale è un ASCII \es 0x5c. Come si è visto, ci sono 5 tali codifiche supportate in MySQL 5.6 di default: big5, cp932, gb2312, gbke sjis. Selezioneremo gbkqui.

    Ora, è molto importante notare l'uso di SET NAMESqui. Questo imposta il set di caratteri SUL SERVER . C'è un altro modo di farlo, ma ci arriveremo abbastanza presto.

  2. Il payload

    Il payload che useremo per questa iniezione inizia con la sequenza di byte 0xbf27. In gbk, questo è un carattere multibyte non valido; dentro latin1, è la stringa ¿'. Si noti che in latin1 e gbk , 0x27da solo, è un 'personaggio letterale .

    Abbiamo scelto questo payload perché, se lo chiamassimo addslashes(), inseriremmo un ASCII , \cioè 0x5cprima del 'personaggio. Così saremmo finire con 0xbf5c27, che a sua gbkè una sequenza di due caratteri: 0xbf5cseguito da 0x27. O in altre parole, un personaggio valido seguito da un non fuggito '. Ma non stiamo usando addslashes(). Quindi al passaggio successivo ...

  3. $ Stmt-> execute ()

    La cosa importante da capire qui è che DOP per impostazione predefinita NON fa dichiarazioni preparate vere. Li emula (per MySQL). Pertanto, PDO crea internamente la stringa di query, chiamando mysql_real_escape_string()(la funzione API MySQL C) su ciascun valore di stringa associata.

    La chiamata dell'API C mysql_real_escape_string()differisce dal fatto addslashes()che conosce il set di caratteri di connessione. Quindi può eseguire correttamente l'escaping per il set di caratteri che il server si aspetta. Tuttavia, fino a questo punto, il client pensa che stiamo ancora usando latin1per la connessione, perché non l'abbiamo mai detto diversamente. Abbiamo detto al server che stiamo usando gbk, ma il client pensa ancora che lo sia latin1.

    Quindi la chiamata a mysql_real_escape_string()inserire la barra rovesciata e abbiamo un 'carattere sospeso nel nostro contenuto "evaso"! In effetti, se dovessimo guardare $varnel gbkset di caratteri, vedremmo:

    縗 'O 1 = 1 / *

    È esattamente ciò che richiede l'attacco.

  4. The Query

    Questa parte è solo una formalità, ma ecco la query di rendering:

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1

Congratulazioni, hai appena attaccato con successo un programma usando le dichiarazioni preparate DOP ...

La soluzione semplice

Ora, vale la pena notare che è possibile prevenirlo disabilitando le dichiarazioni preparate emulate:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Questo di solito si tradurrà in una vera istruzione preparata (cioè i dati inviati in un pacchetto separato dalla query). Tuttavia, tieni presente che PDO riporterà silenziosamente all'emulazione delle dichiarazioni che MySQL non può preparare in modo nativo: quelle che può essere elencate nel manuale, ma fai attenzione a selezionare la versione del server appropriata).

La correzione corretta

Il problema qui è che non abbiamo chiamato le API C mysql_set_charset()invece di SET NAMES. Se lo facessimo, staremmo bene purché utilizziamo una versione di MySQL dal 2006.

Se stai usando una versione di MySQL in precedenza, poi un bug nel mysql_real_escape_string()fatto sì che i caratteri multibyte non validi, come quelli nel nostro payload sono stati trattati come singoli byte per scopi sfuggire anche se il cliente era stato correttamente informato della codifica di collegamento e così questo attacco sarebbe ci riesco ancora. Il bug è stato corretto in MySQL 4.1.20 , 5.0.22 e 5.1.11 .

Ma la parte peggiore è che PDOnon ha esposto l'API C mysql_set_charset()fino alla 5.3.6, quindi nelle versioni precedenti non può impedire questo attacco per ogni possibile comando! Ora è esposto come parametro DSN , che dovrebbe essere usato al posto di SET NAMES ...

La grazia salvifica

Come abbiamo detto all'inizio, per far funzionare questo attacco, la connessione al database deve essere codificata usando un set di caratteri vulnerabili. nonutf8mb4 è vulnerabile e tuttavia può supportare tutti i caratteri Unicode: quindi puoi scegliere di usarlo invece - ma è disponibile solo da MySQL 5.5.3. Un'alternativa è utf8che non è anche vulnerabile e può supportare l'intero piano multilingue Unicode .

In alternativa, è possibile abilitare la NO_BACKSLASH_ESCAPESmodalità SQL, che (tra le altre cose) altera il funzionamento di mysql_real_escape_string(). Con questa modalità abilitata, 0x27verrà sostituita con 0x2727anziché anziché 0x5c27e quindi il processo di escape non può creare caratteri validi in nessuna delle codifiche vulnerabili in cui non esistevano in precedenza (ovvero 0xbf27è ancora 0xbf27ecc.) - quindi il server rifiuterà comunque la stringa come non valida . Tuttavia, vedi la risposta di @ eggyal per una diversa vulnerabilità che può derivare dall'uso di questa modalità SQL (anche se non con PDO).

Esempi sicuri

I seguenti esempi sono sicuri:

mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Perché il server si aspetta utf8...

mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Perché abbiamo impostato correttamente il set di caratteri in modo che il client e il server corrispondano.

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Perché abbiamo disattivato le dichiarazioni preparate emulate.

$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Perché abbiamo impostato correttamente il set di caratteri.

$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

Perché MySQLi fa sempre delle vere dichiarazioni preparate.

Avvolgendo

Se tu:

  • Usa le versioni moderne di MySQL (fine 5.1, tutte le 5.5, 5.6, ecc.) E il parametro charset DSN del PDO (in PHP ≥ 5.3.6)

O

  • Non utilizzare un set di caratteri vulnerabili per la codifica della connessione (usi solo utf8/ latin1/ ascii/ etc)

O

  • Abilita NO_BACKSLASH_ESCAPESmodalità SQL

Sei sicuro al 100%.

Altrimenti, sei vulnerabile anche se stai usando dichiarazioni preparate DOP ...

appendice

Ho lavorato lentamente su una patch per modificare l'impostazione predefinita per non emulare i preparativi per una versione futura di PHP. Il problema che sto incontrando è che un sacco di test si interrompono quando lo faccio. Un problema è che i preparativi emulati genereranno solo errori di sintassi durante l'esecuzione, ma i preparativi veri genereranno errori durante la preparazione. Ciò può causare problemi (ed è parte del motivo per cui i test stanno funzionando).


47
Questa è la risposta migliore che ho trovato ..puoi fornire un link per ulteriori riferimenti?
Statico variabile

1
@nicogawenda: quello era un bug diverso. Prima della 5.0.22, mysql_real_escape_stringnon gestiva correttamente i casi in cui la connessione era impostata correttamente su BIG5 / GBK. Quindi in realtà anche chiamare mysql_set_charset()mysql <5.0.22 sarebbe vulnerabile a questo bug! Quindi no, questo post è ancora applicabile alla 5.0.22 (perché mysql_real_escape_string è solo un set di caratteri lontano dalle chiamate mysql_set_charset(), che è ciò di cui questo post parla di bypassare) ...
ircmaxell,

1
@progfa Se lo fa o no, dovresti sempre convalidare il tuo input sul server prima di fare qualsiasi cosa con i dati dell'utente.
Tek,

2
Si prega di notare che NO_BACKSLASH_ESCAPESsi può anche introdurre nuove vulnerabilità: stackoverflow.com/a/23277864/1014813
lepix

2
@slevin "OR 1 = 1" è un segnaposto per quello che vuoi. Sì, sta cercando un valore nel nome, ma immagina che la parte "OR 1 = 1" fosse "UNION SELECT * FROM utenti". Ora controlli la query e come tale puoi abusarne ...
Ircmaxell,

515

Dichiarazioni preparate / query con parametri sono generalmente sufficienti per impedire l' iniezione del 1 ° ordine su tale affermazione * . Se usi sql dinamico non spuntato in qualsiasi altra parte della tua applicazione, sei ancora vulnerabile all'iniezione del 2 ° ordine .

Iniezione del 2 ° ordine significa che i dati sono stati sottoposti a ciclo nel database una volta prima di essere inclusi in una query ed è molto più difficile da eseguire. AFAIK, non vedi quasi mai veri e propri attacchi ingegnerizzati del 2 ° ordine, poiché di solito è più facile per gli aggressori entrare nel social-engineer, ma a volte hai bug del 2 ° ordine che spuntano a causa di 'personaggi extra benigni o simili.

È possibile eseguire un attacco di iniezione del 2 ° ordine quando si può causare la memorizzazione di un valore in un database che verrà successivamente utilizzato come letterale in una query. Ad esempio, supponiamo di inserire le seguenti informazioni come nuovo nome utente durante la creazione di un account su un sito Web (presupponendo MySQL DB per questa domanda):

' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '

Se non ci sono altre restrizioni sul nome utente, un'istruzione preparata assicurerebbe comunque che la query incorporata sopra non venga eseguita al momento dell'inserimento e memorizzerà correttamente il valore nel database. Tuttavia, immagina che in seguito l'applicazione recuperi il tuo nome utente dal database e utilizzi la concatenazione di stringhe per includere quel valore in una nuova query. Potresti vedere la password di qualcun altro. Dal momento che i primi nomi nella tabella degli utenti tendono ad essere amministratori, è possibile che tu abbia appena distribuito la farm. (Nota anche: questo è un motivo in più per non memorizzare le password in testo semplice!)

Vediamo, quindi, che le istruzioni preparate sono sufficienti per una singola query, ma da soli non siamo , non sufficienti per la protezione contro gli attacchi SQL injection per tutta un'intera applicazione, perché non hanno un meccanismo per far rispettare tutti gli accessi a un database all'interno di un'applicazione utilizza sicura codice. Tuttavia, usato come parte di una buona progettazione dell'applicazione - che può includere pratiche come la revisione del codice o l'analisi statica, o l'uso di un ORM, livello dati o livello di servizio che limita le dinamiche sql - le istruzioni preparate sono lo strumento principale per risolvere l'iniezione Sql problema.Se si seguono buoni principi di progettazione dell'applicazione, in modo tale che l'accesso ai dati sia separato dal resto del programma, diventa facile imporre o controllare che ogni query utilizzi correttamente la parametrizzazione. In questo caso, l'iniezione sql (sia del primo che del secondo ordine) è completamente impedita.


* Risulta che MySql / PHP sono (ok, erano) semplicemente stupidi riguardo alla gestione dei parametri quando sono coinvolti caratteri ampi, e c'è ancora un caso raro delineato nell'altra risposta molto votata qui che può consentire l'iniezione attraverso un parametro query.


6
Interessante. Non ero a conoscenza del 1 ° ordine vs. 2 ° ordine. Puoi approfondire un po 'di più su come funziona il 2 ° ordine?
Mark Biek,

193
Se TUTTE le tue query sono parametrizzate, sei anche protetto contro l'iniezione del 2 ° ordine. L'iniezione del 1 ° ordine sta dimenticando che i dati dell'utente non sono affidabili. L'iniezione del 2 ° ordine sta dimenticando che i dati del database non sono affidabili (perché originariamente forniti dall'utente).
cjm

6
Grazie cjm. Ho anche trovato utile questo articolo nella spiegazione delle iniezioni del 2 ° ordine: codeproject.com/KB/database/SqlInjectionAttacks.aspx
Mark Biek

49
Ah sì. Ma che dire dell'iniezione del terzo ordine . Devono essere consapevoli di quelli.
troelskn,

81
@troelskn che deve essere dove lo sviluppatore è la fonte di dati non affidabili
MikeMurko,

45

No, non lo sono sempre.

Dipende se si consente l'inserimento dell'utente nella query stessa. Per esempio:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

sarebbe vulnerabile alle iniezioni di SQL e l'utilizzo di istruzioni preparate in questo esempio non funzionerà, poiché l'input dell'utente viene utilizzato come identificatore, non come dati. La risposta giusta qui sarebbe quella di utilizzare una sorta di filtro / convalida come:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];
$allowedTables = array('users','admins','moderators');
if (!in_array($tableToUse,$allowedTables))    
 $tableToUse = 'users';

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

Nota: non è possibile utilizzare PDO per associare dati che vanno al di fuori di DDL (Data Definition Language), ovvero che non funziona:

$stmt = $dbh->prepare('SELECT * FROM foo ORDER BY :userSuppliedData');

Il motivo per cui quanto sopra non funziona è perché DESCe ASCnon sono dati . DOP può scappare solo per i dati . In secondo luogo, non è nemmeno possibile inserire 'virgolette attorno ad esso. L'unico modo per consentire l'ordinamento scelto dall'utente è filtrare manualmente e verificare che sia o DESCo ASC.


11
Mi sto perdendo qualcosa qui, ma non è tutto il punto di istruzioni preparate per evitare di trattare sql come una stringa? Non sarebbe qualcosa come $ dbh-> prepar ('SELEZIONA * DA: tableToUse dove username =: username'); aggirare il tuo problema?
Rob Forrest,

4
@RobForrest si ti manca :). I dati associati funzionano solo per DDL (Data Definition Language). Hai bisogno di quelle citazioni e di una corretta fuga. Inserire le virgolette per altre parti della query la interrompe con un'alta probabilità. Ad esempio, SELECT * FROM 'table'può essere sbagliato come dovrebbe essere SELECT * FROM `table`o senza backstick. Poi alcune cose come ORDER BY DESC, dove DESCviene dal l'utente non può essere sfuggito semplicemente. Quindi, gli scenari pratici sono piuttosto illimitati.
Torre,

8
Mi chiedo come 6 persone possano votare un commento proponendo un uso chiaramente sbagliato di una dichiarazione preparata. Se lo avessero provato una volta, avrebbero scoperto subito che l'uso del parametro named al posto del nome di una tabella non funzionava.
Félix Gagnon-Grenier,

Ecco un ottimo tutorial su DOP se vuoi impararlo. a2znotes.blogspot.in/2014/09/introduction-to-pdo.html
RN Kushwaha,

11
Non dovresti mai usare una stringa di query / corpo POST per scegliere la tabella da usare. Se non si dispone di modelli, utilizzare almeno a switchper derivare il nome della tabella.
ZiggyTheHamster,

29

Sì, è sufficiente Il modo in cui funzionano gli attacchi di tipo iniezione sta in qualche modo convincendo un interprete (il database) a valutare qualcosa, che avrebbe dovuto essere un dato, come se fosse un codice. Ciò è possibile solo se si mescolano codice e dati nello stesso supporto (ad es. Quando si costruisce una query come stringa).

Le query con parametri funzionano inviando il codice e i dati separatamente, quindi non sarebbe mai possibile trovare un buco in questo.

Tuttavia, puoi comunque essere vulnerabile ad altri attacchi di tipo iniezione. Ad esempio, se usi i dati in una pagina HTML, potresti essere soggetto ad attacchi di tipo XSS.


10
"Mai" è modo esagerando esso, al punto di essere fuorviante. Se stai usando le istruzioni preparate in modo errato, non è molto meglio che non usarle affatto. (Naturalmente, una "istruzione preparata" che ha iniettato l'input dell'utente in esso annulla lo scopo ... ma in realtà l'ho visto fatto. E le istruzioni preparate non possono gestire gli identificatori (nomi di tabella ecc.) Come parametri.) Aggiungi a ciò, alcuni dei driver PDO emulano le istruzioni preparate e c'è spazio per loro in modo errato (ad esempio, analizzando a metà l'SQL). Versione breve: non dare mai per scontato che sia così facile.
cHao,

29

No, questo non è abbastanza (in alcuni casi specifici)! Per impostazione predefinita, PDO utilizza istruzioni preparate emulate quando si utilizza MySQL come driver di database. Dovresti sempre disabilitare le istruzioni preparate emulate quando usi MySQL e PDO:

$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Un'altra cosa da fare è sempre impostare la codifica corretta del database:

$dbh = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');

Vedi anche questa domanda correlata: come posso impedire l'iniezione di SQL in PHP?

Si noti inoltre che ciò riguarda solo il lato del database delle cose che dovresti ancora guardare durante la visualizzazione dei dati. Ad esempio, usando di htmlspecialchars()nuovo con lo stile di codifica e quotazione corretto.


14

Personalmente eseguirò sempre prima una qualche forma di risanamento sui dati poiché non puoi mai fidarti dell'input dell'utente, tuttavia quando si utilizzano i segnaposto / l'associazione dei parametri i dati immessi vengono inviati al server separatamente all'istruzione sql e quindi associati. La chiave qui è che questo lega i dati forniti a un tipo specifico e ad un uso specifico ed elimina ogni opportunità di cambiare la logica dell'istruzione SQL.


1

Tuttavia, se hai intenzione di prevenire il front-end dell'iniezione sql, usando i controlli html o js, ​​dovresti considerare che i controlli front-end sono "bypassabili".

Puoi disabilitare js o modificare un pattern con uno strumento di sviluppo front-end (integrato con firefox o chrome al giorno d'oggi).

Quindi, al fine di prevenire l'iniezione di SQL, sarebbe giusto disinfettare il backend della data di input all'interno del controller.

Vorrei suggerirvi di usare la funzione PHP nativa filter_input () per disinfettare i valori GET e INPUT.

Se vuoi andare avanti con la sicurezza, per query di database sensibili, vorrei suggerirti di utilizzare l'espressione regolare per convalidare il formato dei dati. preg_match () ti aiuterà in questo caso! Ma stai attento! Il motore Regex non è così leggero. Usalo solo se necessario, altrimenti le prestazioni dell'applicazione diminuiranno.

La sicurezza ha un costo, ma non spreca le tue prestazioni!

Esempio semplice:

se vuoi ricontrollare se un valore, ricevuto da GET è un numero, meno di 99 se (! preg_match ('/ [0-9] {1,2} /')) {...} è più pesante di

if (isset($value) && intval($value)) <99) {...}

Quindi, la risposta finale è: "No! Dichiarazioni preparate DOP non impedisce tutti i tipi di iniezione sql"; Non impedisce valori imprevisti, ma solo una concatenazione imprevista


5
Stai confondendo l'iniezione di SQL con qualcos'altro che rende la tua risposta completamente irrilevante
tuo senso comune
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.