Pertanyaan Bagaimana saya bisa mencegah injeksi SQL di PHP?


Jika input pengguna dimasukkan tanpa modifikasi ke dalam permintaan SQL, maka aplikasi menjadi rentan Injeksi SQL, seperti pada contoh berikut:

$unsafe_variable = $_POST['user_input']; 

mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");

Itu karena pengguna dapat memasukkan sesuatu seperti value'); DROP TABLE table;--, dan kueri menjadi:

INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')

Apa yang bisa dilakukan untuk mencegah hal ini terjadi?


2781


asal


Jawaban:


Gunakan pernyataan siap dan pertanyaan parameter. Ini adalah pernyataan SQL yang dikirim ke dan diurai oleh server database secara terpisah dari parameter apa pun. Dengan cara ini tidak mungkin bagi penyerang untuk menyuntikkan SQL jahat.

Pada dasarnya Anda memiliki dua opsi untuk mencapai ini:

  1. Menggunakan PDO (untuk driver database yang didukung):

    $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    
    $stmt->execute(array('name' => $name));
    
    foreach ($stmt as $row) {
        // do something with $row
    }
    
  2. Menggunakan MySQLi (untuk MySQL):

    $stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
    $stmt->bind_param('s', $name); // 's' specifies the variable type => 'string'
    
    $stmt->execute();
    
    $result = $stmt->get_result();
    while ($row = $result->fetch_assoc()) {
        // do something with $row
    }
    

Jika Anda terhubung ke database selain MySQL, ada opsi kedua khusus pengemudi yang dapat Anda rujuk (mis. pg_prepare() dan pg_execute() untuk PostgreSQL). PDO adalah opsi universal.

Menyetel koneksi dengan benar

Perhatikan bahwa saat menggunakan PDO untuk mengakses database MySQL nyata pernyataan yang disiapkan adalah tidak digunakan secara default. Untuk memperbaikinya Anda harus menonaktifkan emulasi pernyataan yang disiapkan. Contoh membuat koneksi menggunakan PDO adalah:

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

$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

Dalam contoh di atas, mode kesalahan tidak benar-benar diperlukan, tetapi disarankan untuk menambahkannya. Dengan cara ini skrip tidak akan berhenti dengan Fatal Error ketika ada yang salah. Dan itu memberi pengembang kesempatan untuk catch kesalahan apa pun yang terjadi thrown sebagai PDOExceptions.

apa yang wajibNamun, adalah yang pertama setAttribute() baris, yang memberitahu PDO untuk menonaktifkan pernyataan dan penggunaan yang telah disiapkan nyata pernyataan siap. Ini memastikan pernyataan dan nilai-nilai tidak diurai oleh PHP sebelum mengirimnya ke server MySQL (memberikan kemungkinan penyerang tidak ada kesempatan untuk menyuntikkan malicious SQL).

Meskipun Anda dapat mengatur charset dalam opsi konstruktor, penting untuk dicatat bahwa versi PHP yang lebih lama (<5.3.6) diam-diam mengabaikan parameter charset di DSN.

Penjelasan

Apa yang terjadi adalah pernyataan SQL yang Anda berikan prepare diurai dan dikompilasi oleh server basis data. Dengan menetapkan parameter (baik a ? atau parameter bernama seperti :name pada contoh di atas) Anda memberi tahu mesin database tempat Anda ingin memfilter. Kemudian ketika Anda menelepon execute, pernyataan siap dikombinasikan dengan nilai parameter yang Anda tentukan.

Yang penting di sini adalah bahwa nilai parameter digabungkan dengan pernyataan yang dikompilasi, bukan string SQL. Injeksi SQL bekerja dengan mengelabui skrip menjadi string berbahaya saat membuat SQL untuk dikirim ke database. Jadi dengan mengirimkan SQL yang sebenarnya secara terpisah dari parameter, Anda membatasi risiko berakhir dengan sesuatu yang tidak Anda inginkan. Setiap parameter yang Anda kirim saat menggunakan pernyataan yang disiapkan hanya akan diperlakukan sebagai string (meskipun mesin database dapat melakukan beberapa pengoptimalan sehingga parameter mungkin berakhir sebagai angka juga, tentu saja). Dalam contoh di atas, jika $namevariabel mengandung 'Sarah'; DELETE FROM employees hasilnya hanya berupa pencarian string "'Sarah'; DELETE FROM employees", dan Anda tidak akan berakhir dengan sebuah meja kosong.

Manfaat lain dari menggunakan pernyataan siap adalah bahwa jika Anda menjalankan pernyataan yang sama berkali-kali dalam sesi yang sama itu hanya akan diuraikan dan dikompilasi sekali, memberi Anda beberapa keuntungan.

Oh, dan karena Anda bertanya tentang bagaimana melakukannya untuk memasukkan, inilah contoh (menggunakan PDO):

$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');

$preparedStatement->execute(array('column' => $unsafeValue));

Dapatkah pernyataan siap digunakan untuk kueri dinamis?

Meskipun Anda masih dapat menggunakan pernyataan siap untuk parameter kueri, struktur kueri dinamis itu sendiri tidak dapat diseset dan fitur kueri tertentu tidak dapat disalin.

Untuk skenario khusus ini, hal terbaik yang harus dilakukan adalah menggunakan filter daftar putih yang membatasi nilai yang mungkin.

// Value whitelist
// $dir can only be 'DESC' otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
   $dir = 'ASC';
}

7938



PERINGATAN:   Kode contoh jawaban ini (seperti kode contoh pertanyaan) menggunakan PHP mysql ekstensi, yang sudah ditinggalkan dalam PHP 5.5.0 dan dihapus seluruhnya dalam PHP 7.0.0.

Jika Anda menggunakan versi PHP terbaru, mysql_real_escape_string opsi yang diuraikan di bawah ini tidak akan tersedia lagi (sekalipun mysqli::escape_string adalah padanan modern). Hari-hari ini mysql_real_escape_string pilihan hanya akan masuk akal untuk kode warisan pada versi lama PHP.


Anda punya dua opsi - keluar dari karakter khusus di Anda unsafe_variable, atau menggunakan query parameter. Keduanya akan melindungi Anda dari injeksi SQL. Kueri parameterisasi dianggap praktik yang lebih baik tetapi akan membutuhkan perubahan ke ekstensi mysql yang lebih baru di PHP sebelum Anda dapat menggunakannya.

Kami akan membahas string dampak rendah yang melarikan diri terlebih dahulu.

//Connect

$unsafe_variable = $_POST["user-input"];
$safe_variable = mysql_real_escape_string($unsafe_variable);

mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

//Disconnect

Lihat juga, detail dari mysql_real_escape_string fungsi.

Untuk menggunakan kueri parameter, Anda harus menggunakan MySQLi bukan MySQL fungsi. Untuk menulis ulang contoh Anda, kami membutuhkan sesuatu seperti berikut ini.

<?php
    $mysqli = new mysqli("server", "username", "password", "database_name");

    // TODO - Check that connection was successful.

    $unsafe_variable = $_POST["user-input"];

    $stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");

    // TODO check that $stmt creation succeeded

    // "s" means the database expects a string
    $stmt->bind_param("s", $unsafe_variable);

    $stmt->execute();

    $stmt->close();

    $mysqli->close();
?>

Fungsi utama yang Anda ingin baca di sana adalah mysqli::prepare.

Juga, seperti yang disarankan orang lain, Anda mungkin merasa berguna / mudah untuk meningkatkan lapisan abstraksi dengan sesuatu seperti itu PDO.

Harap dicatat bahwa kasus yang Anda tanyakan adalah kasus yang cukup sederhana dan bahwa kasus yang lebih kompleks mungkin memerlukan pendekatan yang lebih kompleks. Khususnya:

  • Jika Anda ingin mengubah struktur dari SQL berdasarkan input pengguna, parameter query tidak akan membantu, dan melarikan diri yang dibutuhkan tidak tercakup oleh mysql_real_escape_string. Dalam kasus semacam ini, Anda akan lebih baik menyampaikan masukan pengguna melalui daftar putih untuk memastikan hanya nilai 'aman' yang diizinkan.
  • Jika Anda menggunakan bilangan bulat dari input pengguna dalam suatu kondisi dan mengambil mysql_real_escape_string pendekatan, Anda akan menderita masalah yang dijelaskan oleh Polinomial di komentar di bawah ini. Kasus ini lebih rumit karena bilangan bulat tidak akan dikelilingi oleh tanda kutip, sehingga Anda dapat menangani dengan memvalidasi bahwa input pengguna hanya berisi digit.
  • Ada kemungkinan kasus lain yang tidak saya sadari. Anda mungkin menemukan ini adalah sumber yang berguna untuk beberapa masalah yang lebih halus yang dapat Anda hadapi.

1509



Setiap jawaban di sini hanya mencakup sebagian dari masalah.
Faktanya, ada empat bagian permintaan yang berbeda yang dapat kita tambahkan secara dinamis:

  • Sebuah benang
  • sebuah angka
  • sebuah identifier
  • kata kunci sintaks.

dan pernyataan yang disiapkan hanya mencakup 2 di antaranya

Tetapi kadang-kadang kita harus membuat permintaan kita lebih dinamis, menambahkan operator atau pengenal juga.
Jadi, kita akan membutuhkan teknik perlindungan yang berbeda.

Secara umum, pendekatan perlindungan semacam itu didasarkan pada daftar putih. Dalam hal ini, setiap parameter dinamis harus dikodekan ulang dalam skrip Anda dan dipilih dari kumpulan itu.
Misalnya, untuk melakukan pemesanan dinamis:

$orders  = array("name","price","qty"); //field names
$key     = array_search($_GET['sort'],$orders)); // see if we have such a name
$orderby = $orders[$key]; //if not, first one will be set automatically. smart enuf :)
$query   = "SELECT * FROM `table` ORDER BY $orderby"; //value is safe

Namun, ada cara lain untuk mengamankan pengidentifikasi - melarikan diri. Selama Anda memiliki tanda pengenal yang dicantumkan, Anda dapat keluar dari backticks di dalamnya dengan menggandakannya.

Sebagai langkah lebih lanjut, kita dapat meminjam ide yang benar-benar brilian dalam menggunakan beberapa placeholder (proxy untuk mewakili nilai aktual dalam query) dari pernyataan yang disiapkan dan menciptakan placeholder tipe lain - placeholder identifier.

Jadi, untuk mempersingkat cerita panjang: itu a placeholdertidak pernyataan siap dapat dianggap sebagai peluru perak.

Jadi, rekomendasi umum dapat diutarakan sebagai
Selama Anda menambahkan bagian dinamis ke kueri menggunakan placeholder (dan placeholder ini diproses dengan baik tentu saja), Anda dapat yakin bahwa kueri Anda aman.

Namun, ada masalah dengan kata kunci sintaks SQL (seperti AND, DESC dan semacamnya) tetapi daftar putih tampaknya merupakan satu-satunya pendekatan dalam kasus ini.

Memperbarui

Meskipun ada perjanjian umum tentang praktik terbaik tentang perlindungan injeksi SQL, ada masih banyak praktik buruk juga. Dan beberapa dari mereka terlalu mengakar di benak pengguna PHP. Misalnya, di halaman ini ada (meskipun tidak terlihat oleh sebagian besar pengunjung) lebih dari 80 jawaban yang dihapus - semua dihapus oleh komunitas karena kualitas buruk atau mempromosikan praktik yang buruk dan ketinggalan jaman. Parahnya lagi, beberapa jawaban yang buruk tidak dihapus tetapi agak makmur.

Sebagai contoh, ada (1)  adalah (2)  masih (3)  banyak (4)  jawaban (5), termasuk jawaban kedua yang paling upvoted menyarankan Anda melepaskan string manual - pendekatan usang yang terbukti tidak aman.

Atau ada jawaban yang sedikit lebih baik yang disarankan metode lain dari pemformatan string dan bahkan membanggakannya sebagai obat mujarab akhir. Meskipun tentu saja tidak. Metode ini tidak lebih baik daripada format string biasa namun tetap menyimpan semua kekurangannya: ini berlaku hanya untuk string dan, seperti pemformatan manual lainnya, pada dasarnya opsional, pengukuran non-wajib, rentan terhadap kesalahan manusia dalam bentuk apa pun.

Saya pikir semua ini karena satu takhayul yang sangat tua, yang didukung oleh otoritas seperti itu OWASP atau Manual PHP, yang menyatakan kesetaraan antara apa pun yang "melarikan diri" dan perlindungan dari injeksi SQL.

Terlepas dari apa yang dikatakan manual PHP selama berabad-abad, *_escape_string tidak berarti membuat data aman dan tidak pernah dimaksudkan untuk itu. Selain tidak berguna untuk bagian SQL selain string, pelolosan manual salah karena manual berlawanan dengan otomatis.

Dan OWASP membuatnya semakin buruk, menekankan pada melarikan diri masukan pengguna yang merupakan omong kosong omong kosong: seharusnya tidak ada kata-kata seperti itu dalam konteks perlindungan injeksi. Setiap variabel berpotensi berbahaya - tidak peduli sumbernya! Atau, dengan kata lain - setiap variabel harus diformat dengan benar untuk dimasukkan ke dalam kueri - tidak peduli sumbernya lagi. Ini tujuan yang penting. Saat pengembang mulai memisahkan domba dari kambing (berpikir apakah beberapa variabel tertentu "aman" atau tidak) dia mengambil langkah pertama menuju bencana. Belum lagi bahwa bahkan kata-kata itu menunjukkan pelarian massal di titik masuk, menyerupai fitur kutipan sihir yang sangat - sudah dibenci, ditinggalkan dan dihapus.

Jadi, tidak seperti pernyataan "melarikan diri" apa pun yang disiapkan aku s ukuran yang benar-benar melindungi dari injeksi SQL (bila berlaku).

Jika Anda masih belum yakin, berikut adalah penjelasan selangkah demi selangkah yang saya tulis, Panduan Hitchhiker untuk pencegahan SQL Injection, di mana saya menjelaskan semua hal ini secara detail dan bahkan menyusun bagian yang sepenuhnya didedikasikan untuk praktik buruk dan pengungkapannya.


940



Saya akan merekomendasikan menggunakan PDO (PHP Data Objects) untuk menjalankan query SQL terprogram.

Ini tidak hanya melindungi terhadap injeksi SQL, tetapi juga mempercepat kueri.

Dan dengan menggunakan PDO daripada mysql_, mysqli_, dan pgsql_ fungsi, Anda membuat aplikasi Anda sedikit lebih abstrak dari database, dalam kejadian langka bahwa Anda harus beralih penyedia basis data.


768



Menggunakan PDO dan menyiapkan pertanyaan.

($conn adalah PDO obyek)

$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();

565



Seperti yang Anda lihat, orang menyarankan Anda menggunakan pernyataan siap paling banyak. Itu tidak salah, tetapi ketika permintaan Anda dieksekusi hanya sekali per proses, akan ada penalti kinerja sedikit.

Saya menghadapi masalah ini, tapi saya rasa saya memecahkannya sangat cara canggih - cara yang digunakan peretas untuk menghindari penggunaan tanda kutip. Saya menggunakan ini bersamaan dengan pernyataan yang diemulasikan. Saya menggunakannya untuk mencegah semua jenis serangan injeksi SQL mungkin.

Pendekatan saya:

  • Jika Anda mengharapkan input menjadi bilangan bulat pastikan itu sangat bilangan bulat. Dalam bahasa tipe-variabel seperti PHP, ini adalah ini sangat penting. Anda dapat menggunakan contoh solusi yang sangat sederhana namun kuat ini: sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input); 

  • Jika Anda mengharapkan yang lain dari integer heksikan itu. Jika Anda heksikan, Anda akan lolos dari semua masukan. Di C / C ++ ada fungsi yang disebut mysql_hex_string(), dalam PHP dapat Anda gunakan bin2hex().

    Jangan khawatir tentang itu, string yang lolos akan memiliki ukuran 2x panjang aslinya karena meskipun Anda menggunakannya mysql_real_escape_string, PHP harus mengalokasikan kapasitas yang sama ((2*input_length)+1), yang sama.

  • Metode hex ini sering digunakan ketika Anda mentransfer data biner, tetapi saya tidak melihat alasan mengapa tidak menggunakannya pada semua data untuk mencegah serangan injeksi SQL. Perhatikan bahwa Anda harus menambahkan data dengan 0x atau gunakan fungsi MySQL UNHEX sebagai gantinya.

Jadi, misalnya, kueri:

SELECT password FROM users WHERE name = 'root'

Akan menjadi:

SELECT password FROM users WHERE name = 0x726f6f74

atau

SELECT password FROM users WHERE name = UNHEX('726f6f74')

Hex adalah pelarian yang sempurna. Tidak ada cara untuk menyuntikkan.

Perbedaan antara fungsi UNHEX dan awalan 0x

Ada beberapa diskusi dalam komentar, jadi saya akhirnya ingin memperjelasnya. Kedua pendekatan ini sangat mirip, tetapi keduanya sedikit berbeda dalam beberapa hal:

Awalan ** 0x ** hanya dapat digunakan untuk kolom data seperti char, varchar, teks, blok, biner, dll.
Juga, penggunaannya sedikit rumit jika Anda akan memasukkan string kosong. Anda harus sepenuhnya menggantinya dengan '', atau Anda akan mendapatkan kesalahan.

UNHEX () bekerja apa saja kolom; Anda tidak perlu khawatir tentang string kosong.


Metode Hex sering digunakan sebagai serangan

Perhatikan bahwa metode hex ini sering digunakan sebagai serangan injeksi SQL di mana bilangan bulat sama seperti string dan hanya bisa lolos mysql_real_escape_string. Maka Anda dapat menghindari penggunaan tanda kutip.

Misalnya, jika Anda hanya melakukan sesuatu seperti ini:

"SELECT title FROM article WHERE id = " . mysql_real_escape_string($_GET["id"])

serangan dapat menyuntikkan Anda dengan sangat baik dengan mudah. Pertimbangkan kode yang disuntikkan berikut yang dikembalikan dari skrip Anda:

PILIH ... DI MANA id = -1 union semua pilih table_name dari information_schema.tables

dan sekarang hanya mengekstrak struktur tabel:

SELECT ... WHERE id = -1 union semua pilih column_name dari information_schema.column dimana table_name = 0x61727469636c65

Dan kemudian pilih saja data apa saja yang diinginkan. Bukankah ini keren?

Tetapi jika coder dari situs suntik akan hex itu, tidak ada injeksi akan mungkin karena permintaan akan terlihat seperti ini: SELECT ... WHERE id = UNHEX('2d312075...3635')


498



PENTING

Cara terbaik untuk mencegah SQL Injection digunakan Laporan yang disiapkan  bukannya melarikan diri, sebagai jawaban yang diterima menunjukkan.

Ada perpustakaan seperti Aura.Sql dan EasyDB yang memungkinkan pengembang menggunakan pernyataan siap lebih mudah. Untuk mempelajari lebih lanjut tentang mengapa pernyataan yang disiapkan lebih baik menghentikan injeksi SQL, mengacu pada ini mysql_real_escape_string() memotong dan baru-baru ini memperbaiki kerentanan Unicode SQL Injection di WordPress.

Pencegahan injeksi - mysql_real_escape_string ()

PHP memiliki fungsi yang dibuat khusus untuk mencegah serangan-serangan ini. Yang perlu Anda lakukan adalah menggunakan seteguk fungsi, mysql_real_escape_string.

mysql_real_escape_string mengambil string yang akan digunakan dalam permintaan MySQL dan mengembalikan string yang sama dengan semua upaya injeksi SQL dengan aman lolos. Pada dasarnya, ini akan menggantikan kutipan yang merepotkan itu (') seorang pengguna mungkin masuk dengan pengganti yang aman dari MySQL, sebuah kutipan yang terlewat \'.

CATATAN: Anda harus terhubung ke database untuk menggunakan fungsi ini!

// Hubungkan ke MySQL

$name_bad = "' OR 1'"; 

$name_bad = mysql_real_escape_string($name_bad);

$query_bad = "SELECT * FROM customers WHERE username = '$name_bad'";
echo "Escaped Bad Injection: <br />" . $query_bad . "<br />";


$name_evil = "'; DELETE FROM customers WHERE 1 or username = '"; 

$name_evil = mysql_real_escape_string($name_evil);

$query_evil = "SELECT * FROM customers WHERE username = '$name_evil'";
echo "Escaped Evil Injection: <br />" . $query_evil;

Anda dapat menemukan detail selengkapnya di MySQL - Pencegahan Injeksi SQL.


452