PDO MySQL: Gunakan PDO :: ATTR_EMULATE_PREPARES atau tidak?

117

Inilah yang sejauh ini saya baca PDO::ATTR_EMULATE_PREPARES:

  1. Emulasi persiapan PDO lebih baik untuk kinerja karena persiapan asli MySQL melewati cache kueri .
  2. Persiapan asli MySQL lebih baik untuk keamanan (mencegah SQL Injection) .
  3. Persiapan asli MySQL lebih baik untuk pelaporan kesalahan .

Saya tidak tahu seberapa benar pernyataan ini lagi. Perhatian terbesar saya dalam memilih antarmuka MySQL adalah mencegah SQL Injection. Perhatian kedua adalah kinerja.

Aplikasi saya saat ini menggunakan MySQLi prosedural (tanpa pernyataan yang disiapkan), dan menggunakan cache kueri cukup banyak. Ini jarang akan menggunakan kembali pernyataan yang sudah disiapkan dalam satu permintaan. Saya mulai pindah ke PDO untuk parameter bernama dan keamanan pernyataan yang disiapkan.

saya menggunakan MySQL 5.1.61 danPHP 5.3.2

Haruskah saya biarkan PDO::ATTR_EMULATE_PREPARESdiaktifkan atau tidak? Adakah cara untuk memiliki performa cache kueri dan keamanan pernyataan yang disiapkan?

Andrew Ensley
sumber
3
Secara jujur? Tetap gunakan MySQLi. Jika sudah bekerja menggunakan pernyataan yang disiapkan di bawahnya, PDO pada dasarnya adalah lapisan abstraksi yang tidak berguna. EDIT : PDO sangat berguna untuk aplikasi lapangan hijau di mana Anda tidak yakin database apa yang masuk ke back-end.
jmkeyes
1
Maaf, pertanyaan saya tidak jelas sebelumnya. Saya sudah mengeditnya. Aplikasi tidak menggunakan pernyataan yang disiapkan di MySQLi saat ini; hanya mysqli_run_query (). Dari apa yang saya baca, pernyataan yang disiapkan MySQLi juga melewati cache kueri.
Andrew Ensley

Jawaban:

108

Untuk menjawab kekhawatiran Anda:

  1. MySQL> = 5.1.17 (atau> = 5.1.21 untuk pernyataan PREPAREdan EXECUTE) dapat menggunakan pernyataan yang disiapkan di cache kueri . Jadi versi MySQL + PHP Anda dapat menggunakan pernyataan yang disiapkan dengan cache kueri. Namun, perhatikan peringatan untuk hasil kueri caching di dokumentasi MySQL. Ada banyak jenis kueri yang tidak dapat disimpan dalam cache atau tidak berguna meskipun telah disimpan dalam cache. Menurut pengalaman saya, cache kueri seringkali bukan merupakan kemenangan yang sangat besar. Query dan skema membutuhkan konstruksi khusus untuk memaksimalkan penggunaan cache. Seringkali cache tingkat aplikasi akhirnya diperlukan dalam jangka panjang.

  2. Persiapan asli tidak membuat perbedaan apa pun untuk keamanan. Pernyataan pseudo-prepared masih akan lolos dari nilai parameter kueri, itu hanya akan dilakukan di perpustakaan PDO dengan string alih-alih di server MySQL menggunakan protokol biner. Dengan kata lain, kode PDO yang sama akan sama-sama rentan (atau tidak rentan) terhadap serangan injeksi apa pun EMULATE_PREPARESsetelan Anda . Satu-satunya perbedaan adalah di mana penggantian parameter terjadi - dengan EMULATE_PREPARES, itu terjadi di perpustakaan PDO; tanpaEMULATE_PREPARES , ini terjadi di server MySQL.

  3. Tanpa EMULATE_PREPARESAnda mungkin mendapatkan kesalahan sintaks pada waktu persiapan daripada pada waktu eksekusi; dengan EMULATE_PREPARESAnda hanya akan mendapatkan kesalahan sintaks pada waktu eksekusi karena PDO tidak memiliki permintaan untuk diberikan ke MySQL sampai waktu eksekusi. Perhatikan bahwa ini memengaruhi kode yang akan Anda tulis ! Terutama jika Anda menggunakan PDO::ERRMODE_EXCEPTION!

Pertimbangan tambahan:

  • Ada biaya tetap untuk prepare()(menggunakan pernyataan yang disiapkan asli), jadi prepare();execute()dengan pernyataan yang disiapkan asli mungkin sedikit lebih lambat daripada mengeluarkan kueri tekstual biasa menggunakan pernyataan siap yang diemulasi. Pada banyak sistem database, rencana kueri untuk a di prepare()-cache juga dan dapat dibagikan dengan banyak koneksi, tetapi menurut saya MySQL tidak melakukan ini. Jadi, jika Anda tidak menggunakan kembali objek pernyataan yang Anda siapkan untuk beberapa kueri, eksekusi Anda secara keseluruhan mungkin lebih lambat.

Sebagai rekomendasi akhir , saya pikir dengan versi MySQL + PHP yang lebih lama, Anda harus meniru pernyataan yang sudah disiapkan, tetapi dengan versi terbaru Anda, Anda harus mematikan emulasi.

Setelah menulis beberapa aplikasi yang menggunakan PDO, saya telah membuat fungsi koneksi PDO yang menurut saya merupakan pengaturan terbaik. Anda mungkin harus menggunakan sesuatu seperti ini atau menyesuaikan pengaturan pilihan Anda:

/**
 * Return PDO handle for a MySQL connection using supplied settings
 *
 * Tries to do the right thing with different php and mysql versions.
 *
 * @param array $settings with keys: host, port, unix_socket, dbname, charset, user, pass. Some may be omitted or NULL.
 * @return PDO
 * @author Francis Avila
 */
function connect_PDO($settings)
{
    $emulate_prepares_below_version = '5.1.17';

    $dsndefaults = array_fill_keys(array('host', 'port', 'unix_socket', 'dbname', 'charset'), null);
    $dsnarr = array_intersect_key($settings, $dsndefaults);
    $dsnarr += $dsndefaults;

    // connection options I like
    $options = array(
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
    );

    // connection charset handling for old php versions
    if ($dsnarr['charset'] and version_compare(PHP_VERSION, '5.3.6', '<')) {
        $options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES '.$dsnarr['charset'];
    }
    $dsnpairs = array();
    foreach ($dsnarr as $k => $v) {
        if ($v===null) continue;
        $dsnpairs[] = "{$k}={$v}";
    }

    $dsn = 'mysql:'.implode(';', $dsnpairs);
    $dbh = new PDO($dsn, $settings['user'], $settings['pass'], $options);

    // Set prepared statement emulation depending on server version
    $serverversion = $dbh->getAttribute(PDO::ATTR_SERVER_VERSION);
    $emulate_prepares = (version_compare($serverversion, $emulate_prepares_below_version, '<'));
    $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, $emulate_prepares);

    return $dbh;
}
Francis Avila
sumber
26
Re # 2: pasti nilai-nilai yang diterima MySQL sebagai parameter (ke pernyataan yang disiapkan asli) tidak diurai untuk SQL sama sekali ? Jadi risiko injeksi harus lebih rendah daripada menggunakan emulasi persiapan PDO, di mana setiap kekurangan dalam melarikan diri (misalnya masalah historis yang mysql_real_escape_stringdimiliki dengan karakter multi-byte) masih akan membiarkan seseorang terbuka untuk serangan injeksi?
eggyal
2
@eggyal, Anda membuat asumsi tentang bagaimana pernyataan yang sudah disiapkan diterapkan. PDO mungkin memiliki bug dalam persiapan emulasinya yang melarikan diri, tetapi MySQL mungkin juga memiliki bug. AFAIK, tidak ada masalah yang ditemukan dengan preparasi yang diemulasi yang dapat menyebabkan literal parameter melewati unescaped.
Francis Avila
2
Jawaban yang luar biasa, tapi saya punya pertanyaan: Jika Anda mematikan EMULASI, bukankah eksekusi akan lebih lambat? PHP harus mengirim pernyataan yang telah disiapkan ke MySQL untuk validasi dan baru kemudian mengirim parameternya. Jadi, jika Anda menggunakan pernyataan yang telah disiapkan 5 kali, PHP akan berbicara dengan MySQL 6 kali (bukan 5 kali). Bukankah ini akan membuatnya lebih lambat? Selain itu, saya pikir ada kemungkinan lebih besar bahwa PDO dapat memiliki bug dalam proses validasi, daripada MySQL ...
Radu Murzea
6
Perhatikan poin-poin yang dibuat dalam jawaban ini, siapkan emulasi pernyataan menggunakan mysql_real_escape_stringunder the hood dan konsekuensi kerentanan yang dapat muncul (dalam kasus edge yang sangat khusus).
eggyal
6
+1 Jawaban bagus! Namun sebagai catatan, jika Anda menggunakan preparasi asli, parameter tidak pernah di-escape atau digabungkan ke dalam kueri SQL bahkan di sisi server MySQL. Pada saat Anda mengeksekusi dan menyediakan parameter, kueri telah diurai dan diubah menjadi struktur data internal di MySQL. Baca blog ini oleh insinyur pengoptimal MySQL yang menjelaskan proses ini: guilhembichot.blogspot.com/2014/05/… Saya tidak mengatakan ini berarti persiapan asli lebih baik, sejauh kami mempercayai kode PDO untuk melakukan pelarian dengan benar (yang saya melakukan).
Bill Karwin
9

Berhati-hatilah saat menonaktifkan PDO::ATTR_EMULATE_PREPARES(mengaktifkan persiapan asli) saat PHP Anda pdo_mysqltidak dikompilasi mysqlnd.

Karena lama libmysqltidak sepenuhnya kompatibel dengan beberapa fungsi, ini dapat menyebabkan bug aneh, misalnya:

  1. Kehilangan bit paling signifikan untuk integer 64bit saat mengikat sebagai PDO::PARAM_INT(0x12345678AB akan dipangkas menjadi 0x345678AB pada mesin 64bit)
  2. Ketidakmampuan untuk membuat kueri sederhana seperti LOCK TABLES (itu melempar SQLSTATE[HY000]: General error: 2030 This command is not supported in the prepared statement protocol yetpengecualian)
  3. Perlu mengambil semua baris dari hasil atau menutup kursor sebelum kueri berikutnya (dengan mysqlnd atau menyiapkannya, secara otomatis melakukan ini untuk Anda dan tidak keluar dari sinkronisasi dengan server mysql)

Bug ini saya temukan dalam proyek sederhana saya ketika bermigrasi ke server lain yang digunakan libmysqluntuk pdo_mysqlmodul. Mungkin masih banyak lagi bug, saya tidak tahu. Saya juga menguji pada debian jessie 64bit baru, semua bug yang terdaftar terjadi ketika saya apt-get install php5-mysql, dan hilang ketika saya apt-get install php5-mysqlnd.

Ketika PDO::ATTR_EMULATE_PREPARESdisetel ke true (sebagai default) - bug ini tidak terjadi, karena PDO tidak menggunakan pernyataan yang disiapkan sama sekali dalam mode ini. Jadi, jika Anda menggunakan substring pdo_mysqlberdasarkan libmysql("mysqlnd" tidak muncul di kolom "Versi API Klien" pdo_mysqldi bagian phpinfo) - Anda tidak boleh PDO::ATTR_EMULATE_PREPARESmematikannya.

Penunjuk Sage
sumber
3
apakah kekhawatiran ini masih berlaku di 2019 ?!
oldboy
8

Saya akan mematikan emulasi persiapan saat Anda menjalankan 5.1 yang berarti PDO akan memanfaatkan fungsionalitas pernyataan asli yang disiapkan.

PDO_MYSQL akan memanfaatkan dukungan pernyataan yang disiapkan asli yang ada di MySQL 4.1 dan lebih tinggi. Jika Anda menggunakan versi lama dari pustaka klien mysql, PDO akan meniru mereka untuk Anda.

http://php.net/manual/en/ref.pdo-mysql.php

Saya membuang MySQLi untuk PDO untuk pernyataan bernama yang disiapkan dan API yang lebih baik.

Namun, untuk diseimbangkan, PDO berkinerja lebih lambat daripada MySQLi, tetapi itu perlu diingat. Saya tahu ini ketika saya membuat pilihan, dan memutuskan bahwa API yang lebih baik dan menggunakan standar industri lebih penting daripada menggunakan pustaka yang lebih cepat dan dapat diabaikan yang mengikat Anda ke mesin tertentu. FWIW Saya pikir tim PHP juga melihat PDO lebih baik daripada MySQLi untuk masa depan juga.

Will Morgan
sumber
Terima kasih atas informasinya. Bagaimana tidak dapat menggunakan cache kueri memengaruhi kinerja Anda atau apakah Anda bahkan menggunakannya sebelumnya?
Andrew Ensley
Saya tidak bisa mengatakan sebagai kerangka kerja saya menggunakan cache pada berbagai tingkatan. Anda selalu dapat menggunakan SELECT SQL_CACHE <sisa pernyataan> secara eksplisit.
Will Morgan
Bahkan tidak tahu ada opsi SELECT SQL_CACHE. Namun, tampaknya itu tetap tidak berhasil. Dari dokumen: "Hasil kueri disimpan dalam cache jika dapat disimpan dalam cache ..." dev.mysql.com/doc/refman/5.1/en/query-cache-in-select.html
Andrew Ensley
Iya. Itu tergantung pada sifat kueri, bukan pada spesifikasi platform.
Will Morgan
Saya membacanya yang berarti "Hasil kueri di-cache kecuali ada hal lain yang mencegahnya agar tidak dapat di-cache ," yang - dari apa yang telah saya baca sampai saat itu - termasuk pernyataan yang disiapkan. Namun, berkat jawaban Francis Avila, saya tahu bahwa itu tidak berlaku lagi untuk versi MySQL saya.
Andrew Ensley
6

Saya akan merekomendasikan mengaktifkan PREPAREpanggilan database nyata karena emulasi tidak menangkap semuanya .., misalnya, itu akan dipersiapkan INSERT;!

var_dump($dbh->prepare('INSERT;'));
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
var_dump($dbh->prepare('INSERT;'));

Hasil

object(PDOStatement)#2 (1) {
  ["queryString"]=>
  string(7) "INSERT;"
}
bool(false)

Saya dengan senang hati akan menerima pukulan kinerja untuk kode yang benar-benar berfungsi.

FWIW

Versi PHP: PHP 5.4.9-4ubuntu2.4 (cli)

Versi MySQL: 5.5.34-0ubuntu0

quickshiftin
sumber
Itu hal yang menarik. Saya kira emulasi menunda penguraian sisi server ke fase eksekusi. Meskipun ini bukan masalah besar (SQL yang salah pada akhirnya akan gagal), lebih bersih membiarkan preparemelakukan pekerjaan yang seharusnya. (Selain itu, saya selalu berasumsi bahwa pengurai parameter sisi klien akan memiliki bugnya sendiri.)
Álvaro González
1
IDK jika Anda tertarik, tetapi ini adalah artikel kecil tentang beberapa perilaku palsu lainnya yang saya perhatikan dengan PDO yang membawa saya ke lubang kelinci ini untuk memulai. Tampaknya penanganan beberapa kueri kurang.
quickshiftin
Saya baru saja melihat beberapa perpustakaan migrasi di GitHub ... Apa yang Anda ketahui, yang satu ini hampir sama persis dengan postingan blog saya.
quickshiftin
6

Saya terkejut tidak ada yang menyebutkan salah satu alasan terbesar untuk menonaktifkan persaingan. Dengan emulasi aktif, PDO mengembalikan semua integer dan float sebagai string . Saat Anda mematikan emulasi, bilangan bulat dan pelampung di MySQL menjadi bilangan bulat dan pelampung di PHP.

Untuk informasi lebih lanjut, lihat jawaban yang diterima untuk pertanyaan ini: PHP + PDO + MySQL: bagaimana cara mengembalikan kolom integer dan numerik dari MySQL sebagai integer dan numerik dalam PHP? .

dallin
sumber
5

Mengapa mengubah emulasi menjadi 'false'?

Alasan utama untuk ini adalah karena mesin database melakukan persiapan daripada PDO adalah bahwa kueri dan data aktual dikirim secara terpisah, yang meningkatkan keamanan. Ini berarti ketika parameter diteruskan ke kueri, upaya untuk memasukkan SQL ke dalamnya diblokir, karena pernyataan yang disiapkan MySQL dibatasi untuk satu kueri. Itu berarti bahwa pernyataan yang benar-benar disiapkan akan gagal saat melewati kueri kedua dalam sebuah parameter.

Argumen utama yang menentang penggunaan mesin database untuk persiapan vs PDO adalah dua perjalanan ke server - satu untuk persiapan, dan satu lagi untuk parameter agar bisa dilewati - tetapi saya pikir keamanan tambahan sepadan. Juga, setidaknya dalam kasus MySQL, query caching tidak menjadi masalah sejak versi 5.1.

https://tech.michaelseiler.net/2016/07/04/dont-emulate-prepared-statements-pdo-mysql/

Harry Bosh
sumber
1
Cache kueri tetap hilang : Cache kueri tidak digunakan lagi pada MySQL 5.7.20, dan dihapus di MySQL 8.0.
Álvaro González
0

Untuk catatan

PDO :: ATTR_EMULATE_PREPARES = true

Itu bisa menghasilkan efek samping yang buruk. Itu bisa mengembalikan nilai int sebagai string.

PHP 7.4, pdo dengan mysqlnd.

Menjalankan kueri dengan PDO :: ATTR_EMULATE_PREPARES = true

Kolom:
Jenis id : integer
Nilai: 1

Menjalankan kueri dengan PDO :: ATTR_EMULATE_PREPARES = false

Kolom:
Jenis id : string
Nilai: "1"

Bagaimanapun, nilai desimal selalu dikembalikan sebagai string, terlepas dari konfigurasinya :-(

magallanes
sumber
nilai desimal selalu dikembalikan string adalah satu-satunya cara yang benar
Akal sehat Anda
Ya dari sudut pandang MySQL tapi itu salah di sisi PHP. Baik Java dan C # menganggap Desimal sebagai nilai numerik.
magallanes
Tidak, tidak. Itu semua benar untuk seluruh ilmu komputer. Jika Anda pikir itu salah, maka Anda membutuhkan jenis lain, dengan ketepatan sewenang-wenang
Akal sehat Anda