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:
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
, gbk
e sjis
. Selezioneremo gbk
qui.
Ora, è molto importante notare l'uso di SET NAMES
qui. Questo imposta il set di caratteri SUL SERVER . C'è un altro modo di farlo, ma ci arriveremo abbastanza presto.
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
, 0x27
da solo, è un '
personaggio letterale .
Abbiamo scelto questo payload perché, se lo chiamassimo addslashes()
, inseriremmo un ASCII , \
cioè 0x5c
prima del '
personaggio. Così saremmo finire con 0xbf5c27
, che a sua gbk
è una sequenza di due caratteri: 0xbf5c
seguito da 0x27
. O in altre parole, un personaggio valido seguito da un non fuggito '
. Ma non stiamo usando addslashes()
. Quindi al passaggio successivo ...
$ 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 latin1
per 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 $var
nel gbk
set di caratteri, vedremmo:
縗 'O 1 = 1 / *
È esattamente ciò che richiede l'attacco.
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 PDO
non 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 è utf8
che non è anche vulnerabile e può supportare l'intero piano multilingue Unicode .
In alternativa, è possibile abilitare la NO_BACKSLASH_ESCAPES
modalità SQL, che (tra le altre cose) altera il funzionamento di mysql_real_escape_string()
. Con questa modalità abilitata, 0x27
verrà sostituita con 0x2727
anziché anziché 0x5c27
e quindi il processo di escape non può creare caratteri validi in nessuna delle codifiche vulnerabili in cui non esistevano in precedenza (ovvero 0xbf27
è ancora 0xbf27
ecc.) - 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_ESCAPES
modalità 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).