TL; DR
mysql_real_escape_string()
tidak akan memberikan perlindungan apa pun (dan selanjutnya dapat menyumbat data Anda) jika:
NO_BACKSLASH_ESCAPES
Mode SQL MySQL diaktifkan (yang mungkin terjadi, kecuali jika Anda secara eksplisit memilih mode SQL lain setiap kali Anda terhubung ); dan
literal string SQL Anda dikutip menggunakan "
karakter kutipan ganda .
Ini diajukan sebagai bug # 72458 dan telah diperbaiki di MySQL v5.7.6 (lihat bagian berjudul " The Saving Grace ", di bawah).
Ini adalah satu lagi, (mungkin kurang?) KASUS EDGE !!!
Sebagai penghormatan atas jawaban luar biasa @ ircmaxell (sungguh, ini seharusnya pujian dan bukan plagiarisme!), Saya akan mengadopsi formatnya:
Serangan itu
Dimulai dengan demonstrasi ...
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"'); // could already be set
$var = mysql_real_escape_string('" OR 1=1 -- ');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
Ini akan mengembalikan semua catatan dari test
tabel. Diseksi:
Memilih Mode SQL
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
Seperti yang didokumentasikan dalam String Literals :
Ada beberapa cara untuk memasukkan karakter kutipan dalam string:
A " '
" di dalam string yang dikutip dengan " '
" dapat ditulis sebagai " ''
".
A " "
" di dalam string yang dikutip dengan " "
" dapat ditulis sebagai " ""
".
Awali karakter kutipan dengan karakter escape (“ \
”)
A " '
" di dalam string yang dikutip dengan " "
" tidak perlu perlakuan khusus dan tidak perlu digandakan atau diloloskan. Dengan cara yang sama, " "
" di dalam string yang dikutip dengan " '
" tidak memerlukan perlakuan khusus.
Jika mode SQL server termasuk NO_BACKSLASH_ESCAPES
, maka ketiga opsi ini — yang merupakan pendekatan yang biasa digunakan oleh mysql_real_escape_string()
— tidak tersedia: salah satu dari dua opsi pertama harus digunakan sebagai gantinya. Perhatikan bahwa efek dari peluru keempat adalah bahwa seseorang harus mengetahui karakter yang akan digunakan untuk mengutip literal untuk menghindari munging data seseorang.
Payload
" OR 1=1 --
Payload memulai injeksi ini secara harfiah dengan "
karakter. Tidak ada pengkodean tertentu. Tidak ada karakter khusus. Tidak ada byte yang aneh.
mysql_real_escape_string ()
$var = mysql_real_escape_string('" OR 1=1 -- ');
Untungnya, mysql_real_escape_string()
tidak memeriksa mode SQL dan menyesuaikan perilakunya sesuai. Lihat libmysql.c
:
ulong STDCALL
mysql_real_escape_string(MYSQL *mysql, char *to,const char *from,
ulong length)
{
if (mysql->server_status & SERVER_STATUS_NO_BACKSLASH_ESCAPES)
return escape_quotes_for_mysql(mysql->charset, to, 0, from, length);
return escape_string_for_mysql(mysql->charset, to, 0, from, length);
}
Jadi fungsi dasar yang berbeda escape_quotes_for_mysql()
,, dipanggil jika NO_BACKSLASH_ESCAPES
mode SQL sedang digunakan. Seperti disebutkan di atas, fungsi seperti itu perlu tahu karakter mana yang akan digunakan untuk mengutip literal untuk mengulanginya tanpa menyebabkan karakter kutipan lainnya tidak diulang secara harfiah.
Namun, fungsi ini secara sewenang-wenang mengasumsikan bahwa string akan dikutip menggunakan '
karakter kutipan tunggal . Lihat charset.c
:
/*
Escape apostrophes by doubling them up
// [ deletia 839-845 ]
DESCRIPTION
This escapes the contents of a string by doubling up any apostrophes that
it contains. This is used when the NO_BACKSLASH_ESCAPES SQL_MODE is in
effect on the server.
// [ deletia 852-858 ]
*/
size_t escape_quotes_for_mysql(CHARSET_INFO *charset_info,
char *to, size_t to_length,
const char *from, size_t length)
{
// [ deletia 865-892 ]
if (*from == '\'')
{
if (to + 2 > to_end)
{
overflow= TRUE;
break;
}
*to++= '\'';
*to++= '\'';
}
Jadi, itu membuat "
karakter kutipan ganda tidak tersentuh (dan menggandakan semua '
karakter kutipan tunggal ) terlepas dari karakter sebenarnya yang digunakan untuk mengutip literal ! Dalam kasus kami $var
tetap persis sama dengan argumen yang diberikan kepada mysql_real_escape_string()
-itu seolah-olah tidak ada melarikan diri telah terjadi sama sekali .
The Query
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
Sesuatu yang formalitas, permintaan yang diberikan adalah:
SELECT * FROM test WHERE name = "" OR 1=1 -- " LIMIT 1
Seperti kata teman terpelajar saya: selamat, Anda baru saja berhasil menyerang sebuah program menggunakan mysql_real_escape_string()
...
Keburukan
mysql_set_charset()
tidak dapat membantu, karena ini tidak ada hubungannya dengan set karakter; juga tidak bisa mysqli::real_escape_string()
, karena itu hanya pembungkus yang berbeda di sekitar fungsi yang sama ini.
Masalahnya, jika belum jelas, adalah bahwa panggilan untuk mysql_real_escape_string()
tidak dapat mengetahui dengan karakter apa literal akan dikutip, karena itu diserahkan kepada pengembang untuk memutuskan di lain waktu. Jadi, dalam NO_BACKSLASH_ESCAPES
mode, secara harfiah tidak ada cara bahwa fungsi ini dapat dengan aman melarikan diri dari setiap input untuk digunakan dengan kuotasi sewenang-wenang (setidaknya, bukan tanpa menggandakan karakter yang tidak memerlukan penggandaan dan dengan demikian menghiasi data Anda).
Jelek
Itu semakin buruk. NO_BACKSLASH_ESCAPES
mungkin tidak semua yang biasa di alam liar karena perlunya penggunaannya untuk kompatibilitas dengan SQL standar (misalnya lihat bagian 5.3 dari spesifikasi SQL-92 , yaitu <quote symbol> ::= <quote><quote>
produksi tata bahasa dan kurangnya makna khusus yang diberikan kepada backslash). Selain itu, penggunaannya secara eksplisit direkomendasikan sebagai solusi untuk bug (lama diperbaiki) yang dijelaskan oleh ircmaxell. Siapa tahu, beberapa DBA bahkan mungkin mengkonfigurasinya untuk diaktifkan secara default sebagai cara mencegah penggunaan metode melarikan diri yang salah seperti addslashes()
.
Juga, mode SQL dari koneksi baru diatur oleh server sesuai dengan konfigurasinya (yang SUPER
dapat diubah pengguna kapan saja); dengan demikian, untuk memastikan perilaku server, Anda harus selalu secara eksplisit menentukan mode yang Anda inginkan setelah tersambung.
Rahmat Yang Menyelamatkan
Selama Anda selalu secara eksplisit mengatur mode SQL untuk tidak memasukkan NO_BACKSLASH_ESCAPES
, atau mengutip literal string MySQL menggunakan karakter kutipan tunggal, bug ini tidak dapat memundurkan kepala jeleknya: masing escape_quotes_for_mysql()
- masing tidak akan digunakan, atau asumsi tentang karakter kutipan mana yang perlu diulang akan benar.
Untuk alasan ini, saya menyarankan agar siapa pun yang menggunakan NO_BACKSLASH_ESCAPES
juga mengaktifkan ANSI_QUOTES
mode, karena itu akan memaksa penggunaan kebiasaan string literal yang dikutip tunggal. Perhatikan bahwa ini tidak mencegah injeksi SQL jika literal yang dikutip ganda kebetulan digunakan - itu hanya mengurangi kemungkinan itu terjadi (karena permintaan normal, non-berbahaya akan gagal).
Dalam PDO, baik fungsi ekivalennya PDO::quote()
maupun pernyataan emulator yang disiapkan memanggil mysql_handle_quoter()
— yang melakukan hal ini: memastikan literal yang lolos dikutip dalam tanda kutip tunggal, sehingga Anda dapat yakin bahwa PDO selalu kebal dari bug ini.
Pada MySQL v5.7.6, bug ini telah diperbaiki. Lihat ubah log :
Fungsi ditambahkan atau diubah
Contoh Aman
Diambil bersama dengan bug yang dijelaskan oleh ircmaxell, contoh-contoh berikut ini sepenuhnya aman (dengan asumsi bahwa seseorang menggunakan MySQL lebih dari 4.1.20, 5.0.22, 5.1.11; atau seseorang tidak menggunakan pengkodean koneksi GBK / Big5) :
mysql_set_charset($charset);
mysql_query("SET SQL_MODE=''");
$var = mysql_real_escape_string('" OR 1=1 /*');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
... karena kami telah secara eksplisit memilih mode SQL yang tidak termasuk NO_BACKSLASH_ESCAPES
.
mysql_set_charset($charset);
$var = mysql_real_escape_string("' OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
... karena kami mengutip string literal kami dengan tanda kutip tunggal.
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(["' OR 1=1 /*"]);
... karena pernyataan yang disiapkan PDO kebal dari kerentanan ini (dan ircmaxell juga, asalkan Anda menggunakan PHP≥5.3.6 dan rangkaian karakter telah ditetapkan dengan benar di DSN; atau emulasi pernyataan yang disiapkan telah dinonaktifkan) .
$var = $pdo->quote("' OR 1=1 /*");
$stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");
... karena quote()
fungsi PDO tidak hanya lolos dari literal, tetapi juga mengutipnya (dalam '
karakter tanda kutip tunggal ); perhatikan bahwa untuk menghindari bug ircmaxell dalam kasus ini, Anda harus menggunakan PHP≥5.3.6 dan telah menetapkan set karakter dengan benar di DSN.
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "' OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
... karena pernyataan yang disiapkan MySQLi aman.
Membungkus
Jadi, jika Anda:
- gunakan pernyataan asli yang disiapkan
ATAU
- gunakan MySQL v5.7.6 atau yang lebih baru
ATAU
... maka Anda harus benar-benar aman (kerentanan di luar lingkup string yang keluar).