Jawaban singkatnya adalah TIDAK , PDO mempersiapkan tidak akan membela Anda dari semua kemungkinan serangan SQL-Injection. Untuk kasus tepi tertentu yang tidak jelas.
Saya mengadaptasi jawaban ini untuk berbicara tentang PDO ...
Jawaban panjangnya tidak mudah. Ini didasarkan pada serangan yang ditunjukkan di sini .
Serangan itu
Jadi, mari kita mulai dengan menunjukkan serangan ...
$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));
Dalam keadaan tertentu, itu akan mengembalikan lebih dari 1 baris. Mari kita membedah apa yang terjadi di sini:
Memilih Set Karakter
$pdo->query('SET NAMES gbk');
Agar serangan ini dapat berfungsi, kita perlu pengkodean yang diharapkan server pada koneksi baik untuk disandikan '
seperti dalam ASCII yaitu 0x27
dan untuk memiliki beberapa karakter yang byte terakhirnya adalah ASCII \
yaitu 0x5c
. Ternyata, ada 5 pengkodean tersebut didukung di MySQL 5.6 secara default: big5
, cp932
, gb2312
, gbk
dan sjis
. Kami akan memilih di gbk
sini.
Sekarang, sangat penting untuk mencatat penggunaan di SET NAMES
sini. Ini mengatur karakter yang diatur PADA SERVER . Ada cara lain untuk melakukannya, tetapi kita akan segera sampai di sana.
Payload
Payload yang akan kita gunakan untuk injeksi ini dimulai dengan urutan byte 0xbf27
. Di gbk
, itu adalah karakter multibyte yang tidak valid; di latin1
, itu string ¿'
. Perhatikan bahwa dalam latin1
dan gbk
, 0x27
dengan sendirinya adalah '
karakter literal .
Kami telah memilih payload ini karena, jika kami memanggilnya addslashes()
, kami akan memasukkan ASCII \
yaitu 0x5c
, sebelum '
karakter. Jadi kita akan berakhir dengan 0xbf5c27
, yang gbk
merupakan urutan dua karakter: 0xbf5c
diikuti oleh 0x27
. Atau dengan kata lain, karakter yang valid diikuti oleh yang tidak terhindar '
. Tapi kami tidak menggunakan addslashes()
. Jadi ke langkah selanjutnya ...
$ stmt-> execute ()
Yang penting untuk disadari di sini adalah bahwa PDO secara default TIDAK melakukan pernyataan yang benar. Ini mengemulasi mereka (untuk MySQL). Oleh karena itu, PDO secara internal membangun string kueri, memanggil mysql_real_escape_string()
(fungsi MySQL C API) pada setiap nilai string terikat.
Panggilan C API mysql_real_escape_string()
berbeda dari addslashes()
yang ia tahu set karakter koneksi. Sehingga dapat melakukan pelolosan dengan benar untuk set karakter yang diharapkan oleh server. Namun, hingga saat ini, klien berpikir bahwa kami masih menggunakan latin1
untuk koneksi, karena kami tidak pernah mengatakan sebaliknya. Kami memang memberi tahu server yang kami gunakan gbk
, tetapi klien masih berpikir itu latin1
.
Karenanya panggilan untuk mysql_real_escape_string()
memasukkan backslash, dan kami memiliki '
karakter bebas menggantung di konten "lolos" kami! Bahkan, jika kita melihat $var
di gbk
set karakter, kita akan melihat:
縗 'ATAU 1 = 1 / *
Persis seperti itulah yang dibutuhkan serangan itu.
The Query
Bagian ini hanya formalitas, tetapi inilah permintaan yang diberikan:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
Selamat, Anda baru saja berhasil menyerang sebuah program menggunakan Pernyataan Disiapkan PDO ...
Perbaikan Sederhana
Sekarang, perlu dicatat bahwa Anda dapat mencegahnya dengan menonaktifkan pernyataan yang disiapkan yang ditiru:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Ini biasanya akan menghasilkan pernyataan yang benar disiapkan (yaitu data yang dikirim dalam paket terpisah dari permintaan). Namun, perlu diketahui bahwa PDO akan diam-diam mundur untuk meniru pernyataan bahwa MySQL tidak dapat mempersiapkan secara asli: mereka yang dapat terdaftar dalam manual, tetapi berhati-hatilah untuk memilih versi server yang sesuai).
Perbaikan yang Benar
Masalahnya di sini adalah bahwa kami tidak memanggil API C mysql_set_charset()
sebagai gantinya SET NAMES
. Jika kami melakukannya, kami akan baik-baik saja asalkan kami menggunakan rilis MySQL sejak 2006.
Jika Anda menggunakan rilis MySQL sebelumnya, maka bug di mysql_real_escape_string()
berarti bahwa karakter multibyte yang tidak valid seperti yang ada di payload kami diperlakukan sebagai byte tunggal untuk melarikan diri bahkan jika klien telah diinformasikan dengan benar tentang pengkodean koneksi sehingga serangan ini akan masih berhasil. Bug diperbaiki di MySQL 4.1.20 , 5.0.22 dan 5.1.11 .
Tetapi bagian terburuknya adalah bahwa PDO
tidak mengekspos C API untuk mysql_set_charset()
sampai 5.3.6, jadi dalam versi sebelumnya tidak dapat mencegah serangan ini untuk setiap perintah yang mungkin! Sekarang terbuka sebagai parameter DSN , yang harus digunakan alih-alih SET NAMES
...
Rahmat Yang Menyelamatkan
Seperti yang kami katakan di awal, untuk serangan ini berfungsi koneksi database harus dikodekan menggunakan set karakter yang rentan. utf8mb4
adalah tidak rentan dan belum dapat mendukung setiap karakter Unicode: sehingga Anda bisa memilih untuk menggunakan bahwa alih-alih-tapi itu hanya tersedia sejak MySQL 5.5.3. Alternatifnya adalah utf8
, yang juga tidak rentan dan dapat mendukung keseluruhan Unicode Basic Multane Plane .
Atau, Anda dapat mengaktifkan NO_BACKSLASH_ESCAPES
mode SQL, yang (antara lain) mengubah operasi mysql_real_escape_string()
. Dengan mode ini diaktifkan, 0x27
akan diganti dengan 0x2727
alih - alih 0x5c27
dan karenanya proses melarikan diri tidak dapat membuat karakter yang valid di salah satu pengkodean rentan di mana mereka tidak ada sebelumnya (yaitu 0xbf27
masih 0xbf27
dll) - sehingga server masih akan menolak string sebagai tidak valid . Namun, lihat jawaban @ eggyal untuk kerentanan berbeda yang dapat timbul dari penggunaan mode SQL ini (meskipun tidak dengan PDO).
Contoh Aman
Contoh-contoh berikut ini aman:
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");
Karena server mengharapkan 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");
Karena kami telah mengatur set karakter dengan benar sehingga klien dan server cocok.
$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 /*"));
Karena kita telah mematikan pernyataan yang disiapkan yang ditiru.
$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 /*"));
Karena kita sudah mengatur set karakter dengan benar.
$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();
Karena MySQLi memang benar menyiapkan pernyataan sepanjang waktu.
Membungkus
Jika kamu:
- Gunakan MySQL Versi Modern (akhir 5.1, semua 5.5, 5.6, dll) DAN parameter charset DSN PDO (dalam PHP ≥ 5.3.6)
ATAU
- Jangan gunakan set karakter yang rentan untuk penyandian koneksi (Anda hanya menggunakan
utf8
/ latin1
/ ascii
/ dll)
ATAU
- Aktifkan
NO_BACKSLASH_ESCAPES
mode SQL
Anda 100% aman.
Jika tidak, Anda rentan meskipun Anda menggunakan Pernyataan Disiapkan PDO ...
Tambahan
Saya perlahan-lahan mengerjakan tambalan untuk mengubah default agar tidak meniru persiapan untuk versi PHP yang akan datang. Masalah yang saya hadapi adalah BANYAK tes pecah ketika saya melakukan itu. Satu masalah adalah bahwa prepared yang diemulasi hanya akan melempar kesalahan sintaks pada eksekusi, tetapi true prepares akan melempar kesalahan pada preparasi. Sehingga bisa menimbulkan masalah (dan merupakan bagian dari alasan tes borking).