Apakah pernyataan yang disiapkan PDO cukup untuk mencegah injeksi SQL?

660

Katakanlah saya memiliki kode seperti ini:

$dbh = new PDO("blahblah");

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

Dokumentasi PDO mengatakan:

Parameter untuk pernyataan yang disiapkan tidak perlu dikutip; pengemudi menanganinya untuk Anda.

Apakah hanya itu yang harus saya lakukan untuk menghindari suntikan SQL? apa benar semudah itu?

Anda dapat mengasumsikan MySQL jika ada bedanya. Juga, saya benar-benar hanya ingin tahu tentang penggunaan pernyataan yang disiapkan terhadap injeksi SQL. Dalam konteks ini, saya tidak peduli dengan XSS atau kemungkinan kerentanan lainnya.

Mark Biek
sumber
5
pendekatan yang lebih baik nomor 7 jawaban stackoverflow.com/questions/134099/...
NullPoiиteя

Jawaban:

807

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:

  1. 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, gbkdan sjis. Kami akan memilih di gbksini.

    Sekarang, sangat penting untuk mencatat penggunaan di SET NAMESsini. Ini mengatur karakter yang diatur PADA SERVER . Ada cara lain untuk melakukannya, tetapi kita akan segera sampai di sana.

  2. 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 , 0x27dengan 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 gbkmerupakan urutan dua karakter: 0xbf5cdiikuti oleh 0x27. Atau dengan kata lain, karakter yang valid diikuti oleh yang tidak terhindar '. Tapi kami tidak menggunakan addslashes(). Jadi ke langkah selanjutnya ...

  3. $ 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 latin1untuk 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 $vardi gbkset karakter, kita akan melihat:

    縗 'ATAU 1 = 1 / *

    Persis seperti itulah yang dibutuhkan serangan itu.

  4. 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 PDOtidak 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. utf8mb4adalah 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_ESCAPESmode SQL, yang (antara lain) mengubah operasi mysql_real_escape_string(). Dengan mode ini diaktifkan, 0x27akan diganti dengan 0x2727alih - alih 0x5c27dan karenanya proses melarikan diri tidak dapat membuat karakter yang valid di salah satu pengkodean rentan di mana mereka tidak ada sebelumnya (yaitu 0xbf27masih 0xbf27dll) - 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_ESCAPESmode 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).

ircmaxell
sumber
47
Ini adalah jawaban terbaik yang saya temukan .. dapatkah Anda memberikan tautan untuk referensi lebih lanjut?
StaticVariable
1
@nicogawenda: itu bug yang berbeda. Sebelum 5.0.22, mysql_real_escape_stringtidak akan menangani kasus dengan benar di mana koneksi diatur dengan benar ke BIG5 / GBK. Jadi sebenarnya bahkan memanggil mysql_set_charset()mysql <5.0.22 akan rentan terhadap bug ini! Jadi tidak, postingan ini masih berlaku untuk 5.0.22 (karena mysql_real_escape_string hanya charset untuk panggilan dari mysql_set_charset(), yang mana posting ini berbicara tentang
mem
1
@progfa Baik atau tidak, Anda harus selalu memvalidasi input Anda di server sebelum melakukan sesuatu dengan data pengguna.
Tek
2
Harap dicatat bahwa NO_BACKSLASH_ESCAPESjuga dapat memperkenalkan kerentanan baru: stackoverflow.com/a/23277864/1014813
lepix
2
@slevin the "OR 1 = 1" adalah pengganti untuk apa pun yang Anda inginkan. Ya, ia mencari nilai dalam nama, tetapi bayangkan bagian "ATAU 1 = 1" adalah "UNION SELECT * FROM pengguna". Anda sekarang mengendalikan kueri, dan karenanya dapat menyalahgunakannya ...
ircmaxell
515

Pernyataan yang disiapkan / pertanyaan parameter umumnya cukup untuk mencegah injeksi urutan pertama pada pernyataan itu * . Jika Anda menggunakan sql dinamis yang tidak dicentang di tempat lain di aplikasi Anda, Anda masih rentan terhadap injeksi pesanan kedua .

Injeksi urutan 2 berarti data telah disikluskan melalui basis data satu kali sebelum dimasukkan dalam kueri, dan jauh lebih sulit untuk dilakukan. AFAIK, Anda hampir tidak pernah melihat serangan urutan ke-2 yang direkayasa nyata, karena biasanya penyerang lebih mudah untuk merekayasa sosial dalam perjalanan mereka, tetapi kadang-kadang Anda memiliki bug urutan ke-2 muncul karena 'karakter yang lebih jinak atau serupa.

Anda bisa melakukan serangan injeksi urutan ke-2 saat Anda dapat menyebabkan nilai disimpan dalam database yang kemudian digunakan sebagai literal dalam kueri. Sebagai contoh, katakanlah Anda memasukkan informasi berikut sebagai nama pengguna baru saat membuat akun di situs web (dengan asumsi MySQL DB untuk pertanyaan ini):

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

Jika tidak ada batasan lain pada nama pengguna, pernyataan yang disiapkan masih akan memastikan bahwa kueri tertanam di atas tidak dijalankan pada saat memasukkan, dan menyimpan nilai dengan benar dalam database. Namun, bayangkan bahwa nanti aplikasi mengambil nama pengguna Anda dari database, dan menggunakan penggabungan string untuk memasukkan nilai itu ke kueri baru. Anda mungkin bisa melihat kata sandi orang lain. Karena beberapa nama pertama dalam tabel pengguna cenderung merupakan admin, Anda mungkin juga baru saja memberikan farm. (Perhatikan juga: ini adalah satu lagi alasan untuk tidak menyimpan kata sandi dalam teks biasa!)

Kami melihat, kemudian, bahwa pernyataan yang disiapkan cukup untuk satu permintaan, tetapi dengan sendirinya pernyataan tersebut tidak cukup untuk melindungi terhadap serangan injeksi sql di seluruh aplikasi, karena mereka tidak memiliki mekanisme untuk menegakkan semua akses ke database dalam aplikasi yang menggunakan safe kode. Namun, digunakan sebagai bagian dari desain aplikasi yang baik - yang dapat mencakup praktik-praktik seperti tinjauan kode atau analisis statis, atau penggunaan ORM, lapisan data, atau lapisan layanan yang membatasi pernyataan sql dinamis yang disiapkan adalah alat utama untuk menyelesaikan Sql Injection masalah.Jika Anda mengikuti prinsip-prinsip desain aplikasi yang baik, sehingga akses data Anda dipisahkan dari sisa program Anda, menjadi mudah untuk menegakkan atau mengaudit bahwa setiap kueri dengan benar menggunakan parameterisasi. Dalam hal ini, injeksi sql (urutan pertama dan kedua) sepenuhnya dicegah.


* Ternyata MySql / PHP (oke, dulunya) hanya bodoh tentang penanganan parameter ketika karakter lebar terlibat, dan masih ada kasus yang jarang dijabarkan dalam jawaban pilihan tinggi lainnya di sini yang dapat memungkinkan injeksi untuk melewati parameter pertanyaan.

Joel Coehoorn
sumber
6
Itu menarik. Saya tidak mengetahui pesanan pertama vs. pesanan kedua. Bisakah Anda menguraikan lebih lanjut tentang bagaimana urutan kedua bekerja?
Mark Biek
193
Jika SEMUA pertanyaan Anda parametrized, Anda juga terlindungi dari injeksi urutan kedua. Injeksi pesanan pertama lupa bahwa data pengguna tidak dapat dipercaya. Injeksi orde kedua lupa bahwa data basis data tidak dapat dipercaya (karena asalnya dari pengguna awalnya).
cjm
6
Terima kasih cjm. Saya juga menemukan artikel ini membantu dalam menjelaskan suntikan urutan ke-2: codeproject.com/KB/database/SqlInjectionAttacks.aspx
Mark Biek
49
Ah iya. Tapi bagaimana dengan injeksi urutan ketiga . Harus mewaspadai itu.
troelskn
81
@troelskn yang pasti menjadi tempat pengembang menjadi sumber data yang tidak dapat dipercaya
MikeMurko
45

Tidak, mereka tidak selalu.

Itu tergantung pada apakah Anda mengizinkan input pengguna ditempatkan di dalam kueri itu sendiri. Sebagai contoh:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];

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

akan rentan terhadap injeksi SQL dan menggunakan pernyataan yang disiapkan dalam contoh ini tidak akan berfungsi, karena input pengguna digunakan sebagai pengidentifikasi, bukan sebagai data. Jawaban yang tepat di sini adalah dengan menggunakan semacam penyaringan / validasi seperti:

$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']) );

Catatan: Anda tidak dapat menggunakan PDO untuk mengikat data yang keluar dari DDL (Data Definition Language), artinya ini tidak berfungsi:

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

Alasan mengapa hal di atas tidak berhasil adalah karena DESCdan ASCbukan data . PDO hanya dapat melarikan diri untuk data . Kedua, Anda bahkan tidak bisa memberi 'tanda kutip di sekitarnya. Satu-satunya cara untuk memungkinkan pemilahan yang dipilih pengguna adalah dengan memfilter secara manual dan memeriksa apakah ada DESCatau tidak ASC.

Menara
sumber
11
Apakah saya kehilangan sesuatu di sini tetapi bukankah seluruh pokok pernyataan yang disiapkan untuk menghindari memperlakukan sql seperti string? Bukankah sesuatu seperti $ dbh-> persiapan ('SELECT * FROM: tableToUse where username =: username'); menyiasati masalah Anda?
Rob Forrest
4
@RobForrest ya Anda hilang :). Data yang Anda ikat hanya berfungsi untuk DDL (Data Definition Language). Anda membutuhkan kutipan dan pelarian yang tepat. Menempatkan kutipan untuk bagian lain dari kueri mematahkannya dengan probabilitas tinggi. Misalnya, SELECT * FROM 'table'bisa salah seperti seharusnya SELECT * FROM `table`atau tanpa backsticks. Maka beberapa hal seperti dari ORDER BY DESCmana DESCdatangnya pengguna tidak bisa diloloskan begitu saja. Jadi, skenario praktis agak tidak terbatas.
Menara
8
Saya bertanya-tanya bagaimana 6 orang dapat membenarkan komentar yang mengusulkan penggunaan pernyataan disiapkan yang salah. Jika mereka pernah mencobanya sekali, mereka akan segera menemukan bahwa menggunakan parameter bernama di tempat nama tabel tidak akan berfungsi.
Félix Gagnon-Grenier
Berikut ini adalah tutorial hebat tentang PDO jika Anda ingin mempelajarinya. a2znotes.blogspot.in/2014/09/introduction-to-pdo.html
RN Kushwaha
11
Anda seharusnya tidak pernah menggunakan string kueri / badan POST untuk memilih tabel yang akan digunakan. Jika Anda tidak memiliki model, setidaknya gunakan a switchuntuk mendapatkan nama tabel.
ZiggyTheHamster
29

Ya, itu sudah cukup. Cara serangan tipe injeksi bekerja, adalah dengan cara mendapatkan penerjemah (Database) untuk mengevaluasi sesuatu, yang seharusnya berupa data, seolah-olah itu adalah kode. Ini hanya mungkin jika Anda mencampur kode dan data dalam media yang sama (mis. Saat Anda membuat kueri sebagai string).

Kueri yang diparameterisasi bekerja dengan mengirimkan kode dan data secara terpisah, sehingga tidak akan mungkin menemukan lubang di dalamnya .

Anda masih bisa rentan terhadap serangan tipe injeksi lainnya. Misalnya, jika Anda menggunakan data dalam halaman HTML, Anda bisa terkena serangan tipe XSS.

troelskn
sumber
10
"Tidak pernah" adalah cara melebih-lebihkannya, sampai-sampai menyesatkan. Jika Anda menggunakan pernyataan yang disiapkan secara salah, itu tidak jauh lebih baik daripada tidak menggunakannya sama sekali. (Tentu saja, "pernyataan yang disiapkan" yang memiliki input pengguna disuntikkan ke dalamnya mengalahkan tujuan ... tapi saya benar-benar telah melihatnya selesai. Dan pernyataan yang disiapkan tidak dapat menangani pengidentifikasi (nama tabel dll) sebagai parameter.) Tambahkan untuk itu, beberapa driver PDO meniru pernyataan yang sudah disiapkan, dan ada ruang bagi mereka untuk melakukannya secara tidak benar (misalnya, dengan setengah-setengah mengurai SQL). Versi singkat: tidak pernah menganggap itu semudah itu.
cHao
29

Tidak, ini tidak cukup (dalam beberapa kasus tertentu)! Secara default PDO menggunakan pernyataan disiapkan emulasi ketika menggunakan MySQL sebagai driver database. Anda harus selalu menonaktifkan pernyataan disiapkan emulasi saat menggunakan MySQL dan PDO:

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

Hal lain yang selalu harus dilakukan adalah mengatur pengkodean database yang benar:

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

Lihat juga pertanyaan terkait ini: Bagaimana saya bisa mencegah injeksi SQL dalam PHP?

Perhatikan juga bahwa hanya tentang sisi database dari hal-hal yang masih harus Anda perhatikan sendiri saat menampilkan data. Misalnya dengan menggunakan htmlspecialchars()lagi dengan gaya pengkodean dan kutipan yang benar.

PeeHaa
sumber
14

Secara pribadi saya akan selalu menjalankan beberapa bentuk sanitasi pada data terlebih dahulu karena Anda tidak pernah bisa mempercayai input pengguna, namun ketika menggunakan placeholder / parameter yang mengikat data yang diinput dikirim ke server secara terpisah ke pernyataan sql dan kemudian diikat bersama. Kuncinya di sini adalah bahwa ini mengikat data yang diberikan ke tipe tertentu dan penggunaan tertentu dan menghilangkan peluang untuk mengubah logika pernyataan SQL.

JimmyJ
sumber
1

Eaven jika Anda akan mencegah front-end sql injection, menggunakan html atau js, Anda harus mempertimbangkan bahwa pemeriksaan front-end "bypassable".

Anda dapat menonaktifkan js atau mengedit pola dengan alat pengembangan front-end (built-in dengan firefox atau chrome saat ini).

Jadi, untuk mencegah injeksi SQL, akan menjadi hak untuk membersihkan backend tanggal input di dalam controller Anda.

Saya ingin menyarankan Anda untuk menggunakan fungsi PHP asli filter_input () untuk membersihkan nilai GET dan INPUT.

Jika Anda ingin melanjutkan keamanan, untuk permintaan basis data yang masuk akal, saya ingin menyarankan Anda untuk menggunakan ekspresi reguler untuk memvalidasi format data. preg_match () akan membantu Anda dalam hal ini! Tapi hati-hati! Mesin regex tidak begitu ringan. Gunakan hanya jika perlu, jika tidak, kinerja aplikasi Anda akan berkurang.

Keamanan memiliki biaya, tetapi jangan sia-siakan kinerja Anda!

Contoh mudah:

jika Anda ingin memeriksa ulang apakah suatu nilai, yang diterima dari GET adalah angka, kurang dari 99 jika (! preg_match ('/ [0-9] {1,2} /')) {...} lebih berat dari

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

Jadi, jawaban akhirnya adalah: "Tidak! Pernyataan Disiapkan PDO tidak mencegah semua jenis injeksi sql"; Itu tidak mencegah nilai-nilai tak terduga, hanya gabungan tak terduga

snipershady
sumber
5
Anda membingungkan injeksi SQL dengan sesuatu yang lain yang membuat jawaban Anda benar-benar tidak relevan
Your Common Sense