Injeksi SQL yang berkeliling mysql_real_escape_string ()

644

Apakah ada kemungkinan injeksi SQL bahkan ketika menggunakan mysql_real_escape_string()fungsi?

Pertimbangkan situasi sampel ini. SQL dibuat dalam PHP seperti ini:

$login = mysql_real_escape_string(GetFromPost('login'));
$password = mysql_real_escape_string(GetFromPost('password'));

$sql = "SELECT * FROM table WHERE login='$login' AND password='$password'";

Saya telah mendengar banyak orang mengatakan kepada saya bahwa kode seperti itu masih berbahaya dan mungkin untuk diretas bahkan dengan mysql_real_escape_string()fungsi yang digunakan. Tapi saya tidak bisa memikirkan kemungkinan eksploitasi?

Suntikan klasik seperti ini:

aaa' OR 1=1 --

tidak bekerja.

Apakah Anda tahu kemungkinan injeksi yang akan melewati kode PHP di atas?

Richard Knop
sumber
34
@ThiefMaster - Saya lebih suka untuk tidak memberikan kesalahan verbal seperti pengguna yang tidak valid / kata sandi yang tidak valid ... ini memberitahu pedagang brute force bahwa mereka memiliki ID pengguna yang valid, dan itu hanya kata sandi yang perlu mereka tebak
Mark Baker
18
Ini mengerikan dari sudut pandang kegunaan. Kadang-kadang Anda tidak dapat menggunakan nama panggilan / nama pengguna / email-utama Anda dan melupakannya setelah beberapa waktu atau situs menghapus akun Anda karena tidak aktif. Maka sangat menjengkelkan jika Anda terus mencoba kata sandi dan bahkan mungkin IP Anda diblokir meskipun hanya nama pengguna Anda yang tidak valid.
ThiefMaster
50
Tolong, jangan gunakan mysql_*fungsi dalam kode baru . Mereka tidak lagi dipertahankan dan proses penghinaan telah dimulai. Lihat kotak merah ? Pelajari lebih lanjut tentang pernyataan yang disiapkan , dan gunakan PDO atau MySQLi - artikel ini akan membantu Anda memutuskan yang mana. Jika Anda memilih PDO, berikut adalah tutorial yang bagus .
tereško
13
@machineaddict, sejak 5.5 (yang dirilis baru-baru ini) mysql_*fungsi sudah menghasilkan E_DEPRECATEDperingatan. The ext/mysqlekstensi tidak dipertahankan selama lebih dari 10 tahun. Apakah Anda benar-benar delusi?
tereško
13
@machineaddict Mereka baru saja menghapus ekstensi itu di PHP 7.0 dan belum 2050.
GGG

Jawaban:

379

Pertimbangkan pertanyaan berikut:

$iId = mysql_real_escape_string("1 OR 1=1");    
$sSql = "SELECT * FROM table WHERE id = $iId";

mysql_real_escape_string()tidak akan melindungi Anda dari ini. Fakta bahwa Anda menggunakan tanda kutip tunggal ( ' ') di sekitar variabel Anda di dalam kueri Anda adalah apa yang melindungi Anda terhadap ini. Berikut ini juga merupakan opsi:

$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";
Wesley van Opdorp
sumber
9
Tetapi ini tidak akan menjadi masalah nyata, karena mysql_query()tidak menjalankan banyak pernyataan, bukan?
Pekka
11
@ Pekka, Meskipun contoh biasa adalah DROP TABLE, dalam praktik penyerang lebih cenderung SELECT passwd FROM users. Dalam kasus terakhir, permintaan kedua biasanya dijalankan dengan menggunakan UNIONklausa.
Jacco
58
(int)mysql_real_escape_string- ini tidak masuk akal. Sama sekali tidak berbeda (int). Dan mereka akan menghasilkan hasil yang sama untuk setiap input
zerkms
28
Ini lebih merupakan penyalahgunaan fungsi daripada yang lainnya. Lagi pula, namanya mysql_real_escape_string, bukan mysql_real_escape_integer. Ini tidak berarti digunakan dengan bidang bilangan bulat.
NullUserException
11
@ Circmaxell, Namun jawabannya benar-benar menyesatkan. Jelas pertanyaannya adalah menanyakan tentang isi dalam tanda kutip. "Kutipan tidak ada" bukanlah jawaban untuk pertanyaan ini.
Pacerier
629

Jawaban singkatnya adalah ya, ya ada cara untuk berkelilingmysql_real_escape_string() .

Untuk KASUS TEPI Sangat OBSCURE !!!

Jawaban panjangnya tidak mudah. Ini didasarkan pada serangan yang ditunjukkan di sini .

Serangan itu

Jadi, mari kita mulai dengan menunjukkan serangan ...

mysql_query('SET NAMES gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Dalam keadaan tertentu, itu akan mengembalikan lebih dari 1 baris. Mari kita membedah apa yang terjadi di sini:

  1. Memilih Set Karakter

    mysql_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 . Jika kami menggunakan panggilan ke fungsi C API mysql_set_charset(), kami akan baik-baik saja (pada rilis MySQL sejak 2006). Tetapi lebih lanjut tentang mengapa dalam satu menit ...

  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. mysql_real_escape_string ()

    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 / *

    Yang persis apa serangan itu membutuhkan.

  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 mysql_real_escape_string()...

Keburukan

Itu semakin buruk. PDOdefault untuk meniru pernyataan yang disiapkan dengan MySQL. Itu berarti bahwa di sisi klien, pada dasarnya melakukan sprintf through mysql_real_escape_string()(di C library), yang berarti berikut ini akan menghasilkan suntikan yang sukses:

$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

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).

Jelek

Saya katakan di awal bahwa kita bisa mencegah semua ini jika kita menggunakan mysql_set_charset('gbk')alih-alih SET NAMES gbk. Dan itu benar asalkan Anda 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 .

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.

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 mysql_set_charset() / $mysqli->set_charset()/ 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)

Anda 100% aman.

Jika tidak, Anda rentan meskipun Anda menggunakanmysql_real_escape_string() ...

ircmaxell
sumber
3
Meniru meniru menyiapkan pernyataan untuk MySQL, benarkah? Saya tidak melihat alasan mengapa itu akan melakukan itu karena pengemudi mendukungnya secara asli. Tidak?
netcoder
16
Itu benar. Mereka mengatakan dalam dokumentasi itu tidak. Tetapi dalam kode sumber, itu jelas terlihat dan mudah diperbaiki. Saya menuliskannya hingga ketidakmampuan para devs.
Theodore R. Smith
5
@ TheodoreR.Smith: Tidak mudah untuk memperbaikinya. Saya telah bekerja untuk mengubah default, tetapi gagal memuat banyak tes ketika diaktifkan. Jadi ini adalah perubahan yang lebih besar dari yang terlihat. Saya masih berharap untuk menyelesaikannya pada 5,5 ...
ircmaxell
14
@shadyyx: Tidak, kerentanan yang dideskripsikan tentang artikel tersebut addslashes. Saya mendasarkan kerentanan ini pada yang satu itu. Cobalah sendiri. Dapatkan MySQL 5.0, dan jalankan exploit ini dan lihat sendiri. Sejauh bagaimana memasukkannya ke dalam PUT / GET / POST, itu TRIVIAL. Input data hanyalah stream byte. char(0xBF)hanyalah cara yang mudah dibaca untuk menghasilkan byte. Saya telah memperagakan kerentanan ini secara langsung di depan banyak konferensi. Percayalah pada saya ... Tapi jika tidak, coba sendiri.
Berhasil
5
@shadyyx: Mengenai kelulusan seperti itu dalam $ _GET ... ?var=%BF%27+OR+1=1+%2F%2Adi URL, $var = $_GET['var'];dalam kode, dan Bob adalah paman Anda.
cao
183

TL; DR

mysql_real_escape_string()tidak akan memberikan perlindungan apa pun (dan selanjutnya dapat menyumbat data Anda) jika:

  • NO_BACKSLASH_ESCAPESMode SQL MySQL diaktifkan (yang mungkin terjadi, kecuali jika Anda secara eksplisit memilih mode SQL lain setiap kali Anda terhubung ); dan

  • literal string SQL Anda dikutip menggunakan "karakter kutipan ganda .

Ini diajukan sebagai bug # 72458 dan telah diperbaiki di MySQL v5.7.6 (lihat bagian berjudul " The Saving Grace ", di bawah).

Ini adalah satu lagi, (mungkin kurang?) KASUS EDGE !!!

Sebagai penghormatan atas jawaban luar biasa @ ircmaxell (sungguh, ini seharusnya pujian dan bukan plagiarisme!), Saya akan mengadopsi formatnya:

Serangan itu

Dimulai dengan demonstrasi ...

mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"'); // could already be set
$var = mysql_real_escape_string('" OR 1=1 -- ');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');

Ini akan mengembalikan semua catatan dari testtabel. Diseksi:

  1. Memilih Mode SQL

    mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');

    Seperti yang didokumentasikan dalam String Literals :

    Ada beberapa cara untuk memasukkan karakter kutipan dalam string:

    • A " '" di dalam string yang dikutip dengan " '" dapat ditulis sebagai " ''".

    • A " "" di dalam string yang dikutip dengan " "" dapat ditulis sebagai " """.

    • Awali karakter kutipan dengan karakter escape (“ \”)

    • A " '" di dalam string yang dikutip dengan " "" tidak perlu perlakuan khusus dan tidak perlu digandakan atau diloloskan. Dengan cara yang sama, " "" di dalam string yang dikutip dengan " '" tidak memerlukan perlakuan khusus.

    Jika mode SQL server termasuk NO_BACKSLASH_ESCAPES, maka ketiga opsi ini — yang merupakan pendekatan yang biasa digunakan oleh mysql_real_escape_string()— tidak tersedia: salah satu dari dua opsi pertama harus digunakan sebagai gantinya. Perhatikan bahwa efek dari peluru keempat adalah bahwa seseorang harus mengetahui karakter yang akan digunakan untuk mengutip literal untuk menghindari munging data seseorang.

  2. Payload

    " OR 1=1 -- 

    Payload memulai injeksi ini secara harfiah dengan "karakter. Tidak ada pengkodean tertentu. Tidak ada karakter khusus. Tidak ada byte yang aneh.

  3. mysql_real_escape_string ()

    $var = mysql_real_escape_string('" OR 1=1 -- ');

    Untungnya, mysql_real_escape_string()tidak memeriksa mode SQL dan menyesuaikan perilakunya sesuai. Lihat libmysql.c:

    ulong STDCALL
    mysql_real_escape_string(MYSQL *mysql, char *to,const char *from,
                 ulong length)
    {
      if (mysql->server_status & SERVER_STATUS_NO_BACKSLASH_ESCAPES)
        return escape_quotes_for_mysql(mysql->charset, to, 0, from, length);
      return escape_string_for_mysql(mysql->charset, to, 0, from, length);
    }
    

    Jadi fungsi dasar yang berbeda escape_quotes_for_mysql(),, dipanggil jika NO_BACKSLASH_ESCAPESmode SQL sedang digunakan. Seperti disebutkan di atas, fungsi seperti itu perlu tahu karakter mana yang akan digunakan untuk mengutip literal untuk mengulanginya tanpa menyebabkan karakter kutipan lainnya tidak diulang secara harfiah.

    Namun, fungsi ini secara sewenang-wenang mengasumsikan bahwa string akan dikutip menggunakan 'karakter kutipan tunggal . Lihat charset.c:

    /*
      Escape apostrophes by doubling them up
    
    // [ deletia 839-845 ]
    
      DESCRIPTION
        This escapes the contents of a string by doubling up any apostrophes that
        it contains. This is used when the NO_BACKSLASH_ESCAPES SQL_MODE is in
        effect on the server.
    
    // [ deletia 852-858 ]
    */
    
    size_t escape_quotes_for_mysql(CHARSET_INFO *charset_info,
                                   char *to, size_t to_length,
                                   const char *from, size_t length)
    {
    // [ deletia 865-892 ]
    
        if (*from == '\'')
        {
          if (to + 2 > to_end)
          {
            overflow= TRUE;
            break;
          }
          *to++= '\'';
          *to++= '\'';
        }
    

    Jadi, itu membuat "karakter kutipan ganda tidak tersentuh (dan menggandakan semua 'karakter kutipan tunggal ) terlepas dari karakter sebenarnya yang digunakan untuk mengutip literal ! Dalam kasus kami $vartetap persis sama dengan argumen yang diberikan kepada mysql_real_escape_string()-itu seolah-olah tidak ada melarikan diri telah terjadi sama sekali .

  4. The Query

    mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');

    Sesuatu yang formalitas, permintaan yang diberikan adalah:

    SELECT * FROM test WHERE name = "" OR 1=1 -- " LIMIT 1

Seperti kata teman terpelajar saya: selamat, Anda baru saja berhasil menyerang sebuah program menggunakan mysql_real_escape_string()...

Keburukan

mysql_set_charset()tidak dapat membantu, karena ini tidak ada hubungannya dengan set karakter; juga tidak bisa mysqli::real_escape_string(), karena itu hanya pembungkus yang berbeda di sekitar fungsi yang sama ini.

Masalahnya, jika belum jelas, adalah bahwa panggilan untuk mysql_real_escape_string() tidak dapat mengetahui dengan karakter apa literal akan dikutip, karena itu diserahkan kepada pengembang untuk memutuskan di lain waktu. Jadi, dalam NO_BACKSLASH_ESCAPESmode, secara harfiah tidak ada cara bahwa fungsi ini dapat dengan aman melarikan diri dari setiap input untuk digunakan dengan kuotasi sewenang-wenang (setidaknya, bukan tanpa menggandakan karakter yang tidak memerlukan penggandaan dan dengan demikian menghiasi data Anda).

Jelek

Itu semakin buruk. NO_BACKSLASH_ESCAPESmungkin tidak semua yang biasa di alam liar karena perlunya penggunaannya untuk kompatibilitas dengan SQL standar (misalnya lihat bagian 5.3 dari spesifikasi SQL-92 , yaitu <quote symbol> ::= <quote><quote>produksi tata bahasa dan kurangnya makna khusus yang diberikan kepada backslash). Selain itu, penggunaannya secara eksplisit direkomendasikan sebagai solusi untuk bug (lama diperbaiki) yang dijelaskan oleh ircmaxell. Siapa tahu, beberapa DBA bahkan mungkin mengkonfigurasinya untuk diaktifkan secara default sebagai cara mencegah penggunaan metode melarikan diri yang salah seperti addslashes().

Juga, mode SQL dari koneksi baru diatur oleh server sesuai dengan konfigurasinya (yang SUPERdapat diubah pengguna kapan saja); dengan demikian, untuk memastikan perilaku server, Anda harus selalu secara eksplisit menentukan mode yang Anda inginkan setelah tersambung.

Rahmat Yang Menyelamatkan

Selama Anda selalu secara eksplisit mengatur mode SQL untuk tidak memasukkan NO_BACKSLASH_ESCAPES, atau mengutip literal string MySQL menggunakan karakter kutipan tunggal, bug ini tidak dapat memundurkan kepala jeleknya: masing escape_quotes_for_mysql()- masing tidak akan digunakan, atau asumsi tentang karakter kutipan mana yang perlu diulang akan benar.

Untuk alasan ini, saya menyarankan agar siapa pun yang menggunakan NO_BACKSLASH_ESCAPESjuga mengaktifkan ANSI_QUOTESmode, karena itu akan memaksa penggunaan kebiasaan string literal yang dikutip tunggal. Perhatikan bahwa ini tidak mencegah injeksi SQL jika literal yang dikutip ganda kebetulan digunakan - itu hanya mengurangi kemungkinan itu terjadi (karena permintaan normal, non-berbahaya akan gagal).

Dalam PDO, baik fungsi ekivalennya PDO::quote()maupun pernyataan emulator yang disiapkan memanggil mysql_handle_quoter()— yang melakukan hal ini: memastikan literal yang lolos dikutip dalam tanda kutip tunggal, sehingga Anda dapat yakin bahwa PDO selalu kebal dari bug ini.

Pada MySQL v5.7.6, bug ini telah diperbaiki. Lihat ubah log :

Fungsi ditambahkan atau diubah

Contoh Aman

Diambil bersama dengan bug yang dijelaskan oleh ircmaxell, contoh-contoh berikut ini sepenuhnya aman (dengan asumsi bahwa seseorang menggunakan MySQL lebih dari 4.1.20, 5.0.22, 5.1.11; atau seseorang tidak menggunakan pengkodean koneksi GBK / Big5) :

mysql_set_charset($charset);
mysql_query("SET SQL_MODE=''");
$var = mysql_real_escape_string('" OR 1=1 /*');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');

... karena kami telah secara eksplisit memilih mode SQL yang tidak termasuk NO_BACKSLASH_ESCAPES.

mysql_set_charset($charset);
$var = mysql_real_escape_string("' OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

... karena kami mengutip string literal kami dengan tanda kutip tunggal.

$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(["' OR 1=1 /*"]);

... karena pernyataan yang disiapkan PDO kebal dari kerentanan ini (dan ircmaxell juga, asalkan Anda menggunakan PHP≥5.3.6 dan rangkaian karakter telah ditetapkan dengan benar di DSN; atau emulasi pernyataan yang disiapkan telah dinonaktifkan) .

$var  = $pdo->quote("' OR 1=1 /*");
$stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");

... karena quote()fungsi PDO tidak hanya lolos dari literal, tetapi juga mengutipnya (dalam 'karakter tanda kutip tunggal ); perhatikan bahwa untuk menghindari bug ircmaxell dalam kasus ini, Anda harus menggunakan PHP≥5.3.6 dan telah menetapkan set karakter dengan benar di DSN.

$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "' OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

... karena pernyataan yang disiapkan MySQLi aman.

Membungkus

Jadi, jika Anda:

  • gunakan pernyataan asli yang disiapkan

ATAU

  • gunakan MySQL v5.7.6 atau yang lebih baru

ATAU

  • di samping untuk menggunakan salah satu solusi dalam ringkasan ircmaxell ini, penggunaan setidaknya salah satu dari:

    • PDO;
    • string literal yang dikutip tunggal; atau
    • mengatur secara eksplisit mode SQL yang tidak termasuk NO_BACKSLASH_ESCAPES

... maka Anda harus benar-benar aman (kerentanan di luar lingkup string yang keluar).

eggyal
sumber
10
Jadi, TL; DR akan seperti "ada mode server mysql NO_BACKSLASH_ESCAPES yang dapat menyebabkan suntikan jika Anda tidak menggunakan tanda kutip tunggal.
Common Sense Anda
Saya tidak dapat mengakses bugs.mysql.com/bug.php?id=72458 ; Saya hanya mendapatkan halaman yang ditolak aksesnya. Apakah disembunyikan dari publik karena masalah keamanan? Juga, apakah saya mengerti dengan benar dari jawaban ini bahwa Anda adalah penemu kerentanan? Jika ya, selamat.
Mark Amery
1
Orang tidak seharusnya menggunakan "string terlebih dahulu. SQL mengatakan itu untuk pengidentifikasi. Tapi eh ... hanyalah contoh lain dari MySQL yang mengatakan "standar sekrup, saya akan melakukan apapun yang saya inginkan". (Untungnya, Anda dapat memasukkan ANSI_QUOTESdalam mode untuk memperbaiki kehancuran kutipan. Pengabaian standar yang terbuka, bagaimanapun, adalah masalah yang lebih besar yang mungkin memerlukan langkah-langkah yang lebih parah.)
cHao
2
@DanAllen: jawaban saya sedikit lebih luas, karena Anda dapat menghindari bug khusus ini melalui quote()fungsi PDO — tetapi pernyataan yang disiapkan adalah cara yang jauh lebih aman dan lebih tepat untuk menghindari injeksi secara umum. Tentu saja, jika Anda secara langsung menggabungkan variabel yang tidak terhapus ke dalam SQL Anda, maka Anda tentu saja rentan terhadap injeksi, apa pun metode yang Anda gunakan sesudahnya.
eggyal
1
@eggyall: Sistem kami bergantung pada contoh aman ke-2 di atas. Ada kesalahan, di mana mysql_real_escape_string telah dihilangkan. Memperbaiki mereka yang berada dalam mode darurat tampaknya merupakan jalan yang bijaksana, berharap kita tidak dihabisi sebelum koreksi. Dasar pemikiran saya adalah mengubah pernyataan yang sudah disiapkan akan menjadi proses yang jauh lebih lama yang harus datang setelah. Apakah alasan pernyataan yang disiapkan lebih aman dari fakta bahwa kesalahan tidak menciptakan kerentanan? Dengan kata lain, apakah benar diterapkan contoh ke-2 di atas sama amannya dengan pernyataan yang disiapkan?
DanAllen
18

Yah, tidak ada yang benar-benar bisa melewati itu, selain %wildcard. Ini bisa berbahaya jika Anda menggunakan LIKEpernyataan sebagai penyerang bisa memasukkan %sebagai login jika Anda tidak memfilter itu, dan harus hanya memaksa kata sandi dari salah satu pengguna Anda. Orang sering menyarankan menggunakan pernyataan yang disiapkan untuk membuatnya 100% aman, karena data tidak dapat mengganggu permintaan itu sendiri. Tetapi untuk pertanyaan sederhana seperti itu mungkin akan lebih efisien untuk melakukan sesuatu seperti$login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);

Slava
sumber
2
+1, tetapi wildcard adalah untuk klausa LIKE, bukan persamaan sederhana.
Dor
7
Menurut ukuran apa Anda menganggap penggantian sederhana more efficientdaripada menggunakan pernyataan yang disiapkan? (Pernyataan yang disiapkan selalu berfungsi, pustaka dapat dengan cepat dikoreksi jika terjadi serangan, tidak mengekspos kesalahan manusia [seperti salah ketik mengetik string pengganti lengkap], dan memiliki manfaat kinerja yang signifikan jika pernyataan tersebut digunakan kembali.)
MatBailie
7
@Slava: Anda secara efektif membatasi nama pengguna dan kata sandi untuk karakter saja. Kebanyakan orang yang tahu apa-apa tentang keamanan akan menganggap itu ide yang buruk, karena menyusutkan ruang pencarian. Tentu saja mereka juga akan menganggapnya sebagai ide buruk untuk menyimpan kata sandi cleartext dalam database, tetapi kita tidak perlu memperparah masalah. :)
cHao
2
@ cHao, saran saya hanya menyangkut login. Jelas Anda tidak perlu memfilter kata sandi, maaf itu tidak jelas dinyatakan dalam jawaban saya. Tapi sebenarnya itu ide yang bagus. Menggunakan "ruang pohon bebatuan" alih-alih-untuk-mengingat-dan-mengetik "a4üua3! @V \" ä90; 8f "akan lebih sulit untuk dilakukan. Bahkan menggunakan kamus, ucapkan 3000 kata untuk membantu Anda, mengetahui Anda menggunakan tepat 4 kata - itu masih sekitar 3,3 * 10 ^ 12 kombinasi. :)
Slava
2
@Slava: Saya pernah melihat ide itu sebelumnya; lihat xkcd.com/936 . Masalahnya adalah, matematika tidak begitu cocok. Contoh kata sandi 17-char Anda memiliki kemungkinan 96 ^ 17, dan itu jika Anda lupa umlaut dan membatasi diri pada ASCII yang dapat dicetak. Itu sekitar 4,5x10 ^ 33. Kita benar-benar berbicara satu miliar trilyun kali lebih banyak pekerjaan untuk kekerasan. Bahkan kata sandi ASCII 8-char akan memiliki 7.2x10 ^ 15 kemungkinan - 3 ribu kali lebih banyak.
cao