Dapatkah Pernyataan PDO PHP menerima nama tabel atau kolom sebagai parameter?

243

Mengapa saya tidak bisa meneruskan nama tabel ke pernyataan PDO yang disiapkan?

$stmt = $dbh->prepare('SELECT * FROM :table WHERE 1');
if ($stmt->execute(array(':table' => 'users'))) {
    var_dump($stmt->fetchAll());
}

Apakah ada cara aman lain untuk memasukkan nama tabel ke dalam query SQL? Dengan aman, maksudku aku tidak ingin melakukannya

$sql = "SELECT * FROM $table WHERE 1"
Jrgns
sumber

Jawaban:

212

Nama Tabel dan Kolom TIDAK BISA digantikan oleh parameter dalam PDO.

Dalam hal ini Anda hanya ingin memfilter dan membersihkan data secara manual. Salah satu cara untuk melakukan ini adalah dengan mengirimkan parameter singkat ke fungsi yang akan mengeksekusi kueri secara dinamis dan kemudian menggunakan switch()pernyataan untuk membuat daftar putih nilai yang valid yang akan digunakan untuk nama tabel atau nama kolom. Dengan begitu tidak ada input pengguna yang langsung masuk ke kueri. Jadi misalnya:

function buildQuery( $get_var ) 
{
    switch($get_var)
    {
        case 1:
            $tbl = 'users';
            break;
    }

    $sql = "SELECT * FROM $tbl";
}

Dengan tidak meninggalkan kasing standar atau menggunakan kasing standar yang mengembalikan pesan kesalahan, Anda memastikan bahwa hanya nilai yang ingin Anda gunakan yang digunakan.

Noah Goodrich
sumber
17
+1 untuk opsi daftar putih alih-alih menggunakan metode dinamis apa pun. Alternatif lain mungkin memetakan nama tabel yang dapat diterima ke array dengan kunci yang sesuai dengan input potensial pengguna (mis array('u'=>'users', 't'=>'table', 'n'=>'nonsensitive_data'). Dll.)
Kzqai
4
Membaca ini, terpikir oleh saya bahwa contoh di sini menghasilkan SQL yang tidak valid untuk input yang buruk, karena tidak memiliki default. Jika menggunakan pola ini, Anda harus memberi label salah satu dari Anda casesebagai default, atau menambahkan kasus kesalahan eksplisit sepertidefault: throw new InvalidArgumentException;
IMSoP
3
Saya sedang berpikir sederhana if ( in_array( $tbl, ['users','products',...] ) { $sql = "SELECT * FROM $tbl"; }. Terima kasih untuk idenya.
Phil Tune
2
Saya rindu mysql_real_escape_string(). Mungkin di sini saya dapat mengatakannya tanpa seseorang melompat masuk dan berkata "Tetapi Anda tidak membutuhkannya dengan PDO"
Rolf
Masalah lainnya adalah bahwa nama tabel dinamis merusak inspeksi SQL.
Acyra
143

Untuk memahami mengapa pengikatan nama tabel (atau kolom) tidak berfungsi, Anda harus memahami cara kerja placeholder dalam pernyataan yang disiapkan: mereka tidak hanya diganti sebagai string (lolos secara tepat), dan SQL yang dihasilkan dieksekusi. Alih-alih, DBMS yang diminta untuk "menyiapkan" pernyataan muncul dengan rencana kueri lengkap untuk bagaimana ia akan mengeksekusi kueri itu, termasuk tabel dan indeks yang akan digunakan, yang akan tetap sama terlepas dari bagaimana Anda mengisi placeholder.

Paket untuk SELECT name FROM my_table WHERE id = :valueakan sama apa pun yang Anda gantikan :value, tetapi yang tampaknya serupa SELECT name FROM :table WHERE id = :valuetidak dapat direncanakan, karena DBMS tidak tahu tabel apa yang sebenarnya akan Anda pilih.

Ini bukan sesuatu perpustakaan abstraksi seperti PDO dapat atau harus bekerja di sekitar, baik, karena akan mengalahkan 2 tujuan utama dari pernyataan yang disiapkan: 1) untuk memungkinkan database untuk memutuskan terlebih dahulu bagaimana kueri akan dijalankan, dan menggunakan yang sama rencanakan beberapa kali; dan 2) untuk mencegah masalah keamanan dengan memisahkan logika kueri dari input variabel.

IMSoP
sumber
1
Benar, tetapi tidak menjelaskan emulasi pernyataan persiapan PDO (yang dapat membuat parameterisasi pengidentifikasi objek SQL, meskipun saya masih setuju bahwa mungkin tidak seharusnya).
eggyal
1
@eggyal Saya kira emulasi ini ditujukan untuk membuat fungsionalitas standar berfungsi pada semua rasa DBMS, daripada menambahkan fungsionalitas yang sama sekali baru. Placeholder untuk pengidentifikasi juga membutuhkan sintaks yang berbeda yang tidak didukung langsung oleh DBMS. PDO adalah pembungkus tingkat rendah, dan tidak misalnya menawarkan dan pembuatan SQL untuk TOP/ LIMIT/ OFFSETklausa, jadi ini akan sedikit keluar dari tempatnya sebagai fitur.
IMSoP
13

Saya melihat ini adalah posting lama, tetapi saya merasa berguna dan saya pikir saya akan membagikan solusi yang mirip dengan yang disarankan @kzqai:

Saya memiliki fungsi yang menerima dua parameter seperti ...

function getTableInfo($inTableName, $inColumnName) {
    ....
}

Di dalam saya mengecek terhadap array yang telah saya atur untuk memastikan hanya tabel dan kolom dengan tabel "diberkati" yang dapat diakses:

$allowed_tables_array = array('tblTheTable');
$allowed_columns_array['tblTheTable'] = array('the_col_to_check');

Kemudian pemeriksaan PHP sebelum menjalankan PDO terlihat seperti ...

if(in_array($inTableName, $allowed_tables_array) && in_array($inColumnName,$allowed_columns_array[$inTableName]))
{
    $sql = "SELECT $inColumnName AS columnInfo
            FROM $inTableName";
    $stmt = $pdo->prepare($sql); 
    $stmt->execute();
    $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
Mengenakan
sumber
2
bagus untuk solusi singkat, tetapi mengapa tidak adil$pdo->query($sql)
jscripter
Sebagian besar dari kebiasaan ketika menyiapkan pertanyaan yang harus mengikat variabel. Baca juga panggilan berulang lebih cepat dengan mengeksekusi di sini stackoverflow.com/questions/4700623/pdos-query-vs-execute
Don
tidak ada panggilan berulang dalam contoh
Anda
4

Menggunakan yang pertama tidak secara inheren lebih aman daripada yang terakhir, Anda perlu membersihkan input apakah itu bagian dari array parameter atau variabel sederhana. Jadi saya tidak melihat ada yang salah dengan menggunakan formulir yang terakhir $table, asalkan Anda memastikan bahwa konten $tableaman (alphanum plus menggarisbawahi?) Sebelum menggunakannya.

Adam Bellaire
sumber
Mengingat opsi pertama tidak berfungsi, Anda harus menggunakan beberapa bentuk pembuatan kueri dinamis.
Noah Goodrich
Ya, pertanyaan yang disebutkan tidak akan berhasil. Saya mencoba menjelaskan mengapa itu tidak terlalu penting untuk dilakukan dengan cara itu.
Adam Bellaire
3

(Jawaban terlambat, lihat catatan samping saya).

Aturan yang sama berlaku ketika mencoba membuat "database".

Anda tidak dapat menggunakan pernyataan yang disiapkan untuk mengikat basis data.

Yaitu:

CREATE DATABASE IF NOT EXISTS :database

tidak akan berfungsi. Gunakan daftar aman sebagai gantinya.

Catatan tambahan: Saya menambahkan jawaban ini (sebagai wiki komunitas) karena sering digunakan untuk menutup pertanyaan, di mana beberapa orang memposting pertanyaan yang mirip dengan ini dalam mencoba untuk mengikat database dan bukan tabel dan / atau kolom.

Funk Forty Niner
sumber
0

Sebagian dari saya bertanya-tanya apakah Anda dapat menyediakan fungsi sanitasi sendiri sesederhana ini:

$value = preg_replace('/[^a-zA-Z_]*/', '', $value);

Saya belum benar-benar memikirkannya, tetapi sepertinya menghapus apa pun kecuali karakter dan garis bawah mungkin berfungsi.

Phil LaNasa
sumber
1
Nama tabel MySQL dapat berisi karakter lain. Lihat dev.mysql.com/doc/refman/5.0/en/identifiers.html
Phil
@ PhilLaNasa sebenarnya beberapa pertahanan yang seharusnya (referensi kebutuhan). Karena sebagian besar DBMS peka huruf besar-kecil yang menyimpan nama dalam karakter yang tidak terdiferensiasi, mis: MyLongTableNamemudah dibaca dengan benar, tetapi jika Anda memeriksa nama yang tersimpan itu (kemungkinan) adalah MYLONGTABLENAMEyang tidak terlalu mudah dibaca, jadi MY_LONG_TABLE_NAMEsebenarnya lebih mudah dibaca.
mloureiro
Ada alasan yang sangat bagus untuk tidak memiliki ini sebagai fungsi: Anda harus sangat jarang memilih nama tabel berdasarkan input sewenang-wenang. Anda hampir pasti tidak ingin pengguna jahat mengganti "pengguna" atau "pemesanan" Select * From $table. Daftar putih atau pencocokan pola ketat (mis. "Nama yang memulai laporan_ diikuti hanya oleh 1 hingga 3 digit") sangat penting di sini.
IMSoP
0

Adapun pertanyaan utama di utas ini, postingan lain menjelaskan mengapa kita tidak dapat mengikat nilai ke nama kolom saat menyiapkan pernyataan, jadi di sini ada satu solusi:

class myPdo{
    private $user   = 'dbuser';
    private $pass   = 'dbpass';
    private $host   = 'dbhost';
    private $db = 'dbname';
    private $pdo;
    private $dbInfo;
    public function __construct($type){
        $this->pdo = new PDO('mysql:host='.$this->host.';dbname='.$this->db.';charset=utf8',$this->user,$this->pass);
        if(isset($type)){
            //when class is called upon, it stores column names and column types from the table of you choice in $this->dbInfo;
            $stmt = "select distinct column_name,column_type from information_schema.columns where table_name='sometable';";
            $stmt = $this->pdo->prepare($stmt);//not really necessary since this stmt doesn't contain any dynamic values;
            $stmt->execute();
            $this->dbInfo = $stmt->fetchAll(PDO::FETCH_ASSOC);
        }
    }
    public function pdo_param($col){
        $param_type = PDO::PARAM_STR;
        foreach($this->dbInfo as $k => $arr){
            if($arr['column_name'] == $col){
                if(strstr($arr['column_type'],'int')){
                    $param_type = PDO::PARAM_INT;
                    break;
                }
            }
        }//for testing purposes i only used INT and VARCHAR column types. Adjust to your needs...
        return $param_type;
    }
    public function columnIsAllowed($col){
        $colisAllowed = false;
        foreach($this->dbInfo as $k => $arr){
            if($arr['column_name'] === $col){
                $colisAllowed = true;
                break;
            }
        }
        return $colisAllowed;
    }
    public function q($data){
        //$data is received by post as a JSON object and looks like this
        //{"data":{"column_a":"value","column_b":"value","column_c":"value"},"get":"column_x"}
        $data = json_decode($data,TRUE);
        $continue = true;
        foreach($data['data'] as $column_name => $value){
            if(!$this->columnIsAllowed($column_name)){
                 $continue = false;
                 //means that someone possibly messed with the post and tried to get data from a column that does not exist in the current table, or the column name is a sql injection string and so on...
                 break;
             }
        }
        //since $data['get'] is also a column, check if its allowed as well
        if(isset($data['get']) && !$this->columnIsAllowed($data['get'])){
             $continue = false;
        }
        if(!$continue){
            exit('possible injection attempt');
        }
        //continue with the rest of the func, as you normally would
        $stmt = "SELECT DISTINCT ".$data['get']." from sometable WHERE ";
        foreach($data['data'] as $k => $v){
            $stmt .= $k.' LIKE :'.$k.'_val AND ';
        }
        $stmt = substr($stmt,0,-5)." order by ".$data['get'];
        //$stmt should look like this
        //SELECT DISTINCT column_x from sometable WHERE column_a LIKE :column_a_val AND column_b LIKE :column_b_val AND column_c LIKE :column_c_val order by column_x
        $stmt = $this->pdo->prepare($stmt);
        //obviously now i have to bindValue()
        foreach($data['data'] as $k => $v){
            $stmt->bindValue(':'.$k.'_val','%'.$v.'%',$this->pdo_param($k));
            //setting PDO::PARAM... type based on column_type from $this->dbInfo
        }
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC);//or whatever
    }
}
$pdo = new myPdo('anything');//anything so that isset() evaluates to TRUE.
var_dump($pdo->q($some_json_object_as_described_above));

Di atas hanyalah sebuah contoh, jadi tak perlu dikatakan, copy-> paste tidak akan berfungsi. Sesuaikan dengan kebutuhan Anda. Sekarang ini mungkin tidak memberikan keamanan 100%, tetapi memungkinkan beberapa kontrol atas nama kolom ketika mereka "masuk" sebagai string dinamis dan dapat diubah pada akhir pengguna. Selain itu, tidak perlu untuk membangun beberapa array dengan nama dan tipe kolom tabel Anda karena mereka diambil dari information_schema.

pria
sumber