Injeksi SQL

Berlaku untuk:SQL ServerAzure SQL DatabaseAzure SQL Managed InstanceAzure Synapse Analytics AnalyticsPlatform System (PDW)

Injeksi SQL adalah serangan di mana kode berbahaya dimasukkan ke dalam string yang kemudian diteruskan ke instans Mesin Database SQL Server untuk penguraian dan eksekusi. Prosedur apa pun yang membangun pernyataan SQL harus ditinjau untuk kerentanan injeksi, karena Mesin Database menjalankan semua kueri yang valid secara sintetis yang diterimanya. Bahkan data berparameter dapat dimanipulasi oleh penyerang yang terampil dan ditentukan.

Cara kerja injeksi SQL

Bentuk utama injeksi SQL terdiri dari penyisipan kode langsung ke dalam variabel input pengguna yang digabungkan dengan perintah SQL dan dijalankan. Serangan yang kurang langsung menyuntikkan kode berbahaya ke dalam string yang ditujukan untuk penyimpanan dalam tabel atau sebagai metadata. Ketika string yang disimpan kemudian digabungkan ke dalam perintah SQL dinamis, kode berbahaya dijalankan.

Proses injeksi bekerja dengan menghentikan string teks secara prematur dan menambahkan perintah baru. Karena perintah yang disisipkan mungkin memiliki string tambahan yang ditambahkan sebelum dijalankan, malefactor mengakhiri string yang disuntikkan dengan tanda --komentar . Teks berikutnya diabaikan pada waktu eksekusi.

Skrip berikut menunjukkan injeksi SQL sederhana. Skrip menyusun kueri SQL dengan menggabungkan string yang dikodekan secara permanen bersama dengan string yang dimasukkan oleh pengguna:

var ShipCity;
ShipCity = Request.form ("ShipCity");
var sql = "select * from OrdersTable where ShipCity = '" + ShipCity + "'";

Pengguna diminta untuk memasukkan nama kota. Jika mereka memasukkan Redmond, kueri yang dirakit oleh skrip terlihat mirip dengan contoh berikut:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';

Namun, asumsikan bahwa pengguna memasukkan teks berikut:

Redmond';drop table OrdersTable--

Dalam hal ini, skrip menyusun kueri berikut:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';drop table OrdersTable--'

Titik koma (;) menunjukkan akhir dari satu kueri dan awal kueri lainnya. Tanda hubung ganda (--) menunjukkan bahwa sisa baris saat ini adalah komentar dan harus diabaikan. Jika kode yang dimodifikasi benar secara sintis, kode tersebut dijalankan oleh server. Ketika Mesin Database memproses pernyataan ini, pertama-tama memilih semua rekaman di OrdersTable mana ShipCity adalah Redmond. Kemudian, Mesin Database turun OrdersTable.

Selama kode SQL yang disuntikkan benar secara sintetis, perubahan tidak dapat dideteksi secara terprogram. Oleh karena itu, Anda harus memvalidasi semua input pengguna dan dengan cermat meninjau kode yang menjalankan perintah SQL yang dibangun di server yang Anda gunakan. Praktik terbaik pengodean dijelaskan di bagian berikut dalam artikel ini.

Memvalidasi semua input

Selalu validasi input pengguna dengan menguji jenis, panjang, format, dan rentang. Saat Anda menerapkan tindakan pencegahan terhadap input berbahaya, pertimbangkan skenario arsitektur dan penyebaran aplikasi Anda. Ingatlah bahwa program yang dirancang untuk berjalan di lingkungan yang aman dapat disalin ke lingkungan yang tidak aman. Saran berikut harus dianggap sebagai praktik terbaik:

  • Jangan asumsi tentang ukuran, jenis, atau konten data yang diterima oleh aplikasi Anda. Misalnya, Anda harus membuat evaluasi berikut:

    • Bagaimana aplikasi Anda bereaksi jika pengguna yang salah atau berbahaya memasukkan file video 2 GB di mana aplikasi Anda mengharapkan kode pos?

    • Bagaimana aplikasi Anda bereaksi jika DROP TABLE pernyataan disematkan di bidang teks?

  • Uji ukuran dan jenis data input dan tertibkan batas yang sesuai. Ini dapat membantu mencegah overrun buffer yang disengaja.

  • Uji konten variabel string dan terima hanya nilai yang diharapkan. Tolak entri yang berisi data biner, urutan escape, dan karakter komentar. Ini dapat membantu mencegah injeksi skrip dan dapat melindungi dari beberapa eksploitasi overrun buffer.

  • Saat Anda bekerja dengan dokumen XML, validasi semua data terhadap skemanya saat dimasukkan.

  • Jangan pernah membangun pernyataan Transact-SQL langsung dari input pengguna.

  • Gunakan prosedur tersimpan untuk memvalidasi input pengguna.

  • Di lingkungan multitiered, semua data harus divalidasi sebelum masuk ke zona tepercaya. Data yang tidak lulus proses validasi harus ditolak dan kesalahan harus dikembalikan ke tingkat sebelumnya.

  • Menerapkan beberapa lapisan validasi. Tindakan pencegahan yang Anda lakukan terhadap pengguna berbahaya yang santai mungkin tidak efektif terhadap penyerang yang ditentukan. Praktik yang lebih baik adalah memvalidasi input di antarmuka pengguna dan di semua titik berikutnya di mana melewati batas kepercayaan.

    Misalnya, validasi data dalam aplikasi sisi klien dapat mencegah injeksi skrip sederhana. Namun, jika tingkat berikutnya mengasumsikan bahwa inputnya sudah divalidasi, setiap pengguna berbahaya yang dapat melewati klien dapat memiliki akses tidak terbatas ke sistem.

  • Jangan pernah menggabungkan input pengguna yang tidak divalidasi. Perangkaian string adalah titik entri utama untuk injeksi skrip.

  • Jangan terima string berikut dalam bidang tempat nama file dapat dibangun: AUX, , CLOCK$COM1 melalui COM8, , CONFIG$CON, LPT1 melalui LPT8, , NUL, dan PRN.

Jika bisa, tolak input yang berisi karakter berikut.

Karakter input Arti dalam Transact-SQL
; Pemisah kueri.
' Pemisah string data karakter.
-- Pemisah komentar baris tunggal. Teks berikut -- hingga akhir baris tersebut tidak dievaluasi oleh server.
/*** ... ***/ Pemisah komentar. Teks antara /* dan */ tidak dievaluasi oleh server.
xp_ Digunakan pada awal nama prosedur tersimpan yang diperluas katalog, seperti xp_cmdshell.

Menggunakan parameter SQL yang aman untuk jenis

Koleksi Parameters di Mesin Database menyediakan pemeriksaan jenis dan validasi panjang. Jika Anda menggunakan Parameters koleksi, input diperlakukan sebagai nilai harfiah alih-alih sebagai kode yang dapat dieksekusi. Manfaat lain menggunakan Parameters koleksi adalah Anda dapat memberlakukan pemeriksaan jenis dan panjang. Nilai di luar rentang memicu pengecualian. Fragmen kode berikut menunjukkan penggunaan Parameters koleksi:

SqlDataAdapter myCommand = new SqlDataAdapter("AuthorLogin", conn);
myCommand.SelectCommand.CommandType = CommandType.StoredProcedure;
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
    SqlDbType.VarChar, 11);
parm.Value = Login.Text;

Dalam contoh ini, @au_id parameter diperlakukan sebagai nilai harfiah alih-alih sebagai kode yang dapat dieksekusi. Nilai ini diperiksa untuk jenis dan panjang. Jika nilai tidak mematuhi jenis dan batasan @au_id panjang yang ditentukan, pengecualian akan dilemparkan.

Menggunakan input berparameter dengan prosedur tersimpan

Prosedur tersimpan mungkin rentan terhadap injeksi SQL jika menggunakan input yang tidak difilter. Misalnya, kode berikut rentan:

SqlDataAdapter myCommand =
    new SqlDataAdapter("LoginStoredProcedure '" + Login.Text + "'", conn);

Jika Anda menggunakan prosedur tersimpan, Anda harus menggunakan parameter sebagai inputnya.

Menggunakan koleksi Parameter dengan SQL dinamis

Jika Anda tidak dapat menggunakan prosedur tersimpan, Anda masih dapat menggunakan parameter, seperti yang ditunjukkan dalam contoh kode berikut.

SqlDataAdapter myCommand = new SqlDataAdapter(
    "SELECT au_lname, au_fname FROM Authors WHERE au_id = @au_id", conn);
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
    SqlDbType.VarChar, 11);
parm.Value = Login.Text;

Input filter

Memfilter input mungkin juga berguna dalam melindungi dari injeksi SQL dengan menghapus karakter escape. Namun, karena banyaknya karakter yang mungkin menimbulkan masalah, pemfilteran bukanlah pertahanan yang dapat diandalkan. Contoh berikut mencari pemisah string karakter.

private string SafeSqlLiteral(string inputSQL)
{
    return inputSQL.Replace("'", "''");
}

Klausa LIKE

Jika Anda menggunakan klausul LIKE , karakter kartubebas masih harus dilewati:

s = s.Replace("[", "[[]");
s = s.Replace("%", "[%]");
s = s.Replace("_", "[_]");

Meninjau kode untuk injeksi SQL

Anda harus meninjau semua kode yang memanggil EXECUTE, , EXECatau sp_executesql. Anda dapat menggunakan kueri yang mirip dengan yang berikut ini untuk membantu Anda mengidentifikasi prosedur yang berisi pernyataan ini. Kueri ini memeriksa 1, 2, 3, atau 4 spasi setelah kata EXECUTE atau EXEC.

SELECT object_Name(id)
FROM syscomments
WHERE UPPER(TEXT) LIKE '%EXECUTE (%'
    OR UPPER(TEXT) LIKE '%EXECUTE  (%'
    OR UPPER(TEXT) LIKE '%EXECUTE   (%'
    OR UPPER(TEXT) LIKE '%EXECUTE    (%'
    OR UPPER(TEXT) LIKE '%EXEC (%'
    OR UPPER(TEXT) LIKE '%EXEC  (%'
    OR UPPER(TEXT) LIKE '%EXEC   (%'
    OR UPPER(TEXT) LIKE '%EXEC    (%'
    OR UPPER(TEXT) LIKE '%SP_EXECUTESQL%';

Bungkus parameter dengan QUOTENAME() dan REPLACE()

Dalam setiap prosedur tersimpan yang dipilih, verifikasi bahwa semua variabel yang digunakan dalam Transact-SQL dinamis ditangani dengan benar. Data yang berasal dari parameter input prosedur tersimpan atau yang dibaca dari tabel harus dibungkus atau QUOTENAME()REPLACE(). Ingatlah bahwa nilai @variable yang diteruskan ke QUOTENAME() adalah sysname, dan memiliki panjang maksimum 128 karakter.

@variable Pembungkus yang direkomendasikan
Nama yang dapat diamankan QUOTENAME(@variable)
String = <128 karakter QUOTENAME(@variable, '''')
> String 128 karakter REPLACE(@variable,'''', '''''')

Ketika Anda menggunakan teknik ini, pernyataan SET dapat direvisi sebagai berikut:

-- Before:
SET @temp = N'SELECT * FROM authors WHERE au_lname ='''
    + @au_lname + N'''';

-- After:
SET @temp = N'SELECT * FROM authors WHERE au_lname = '''
    + REPLACE(@au_lname, '''', '''''') + N'''';

Injeksi diaktifkan oleh pemotongan data

Setiap Transact-SQL dinamis yang ditetapkan ke variabel, dipotong jika lebih besar dari buffer yang dialokasikan untuk variabel tersebut. Penyerang yang mampu memaksa pemotongan pernyataan dengan meneruskan untai (karakter) yang tidak terduga panjang ke prosedur tersimpan dapat memanipulasi hasilnya. Misalnya, contoh prosedur tersimpan berikut rentan terhadap injeksi yang diaktifkan oleh pemotongan.

Dalam contoh ini, kita memiliki @command buffer dengan panjang maksimum 200 karakter. Kita membutuhkan total 154 karakter untuk mengatur kata sandi 'sa': 26 untuk UPDATE pernyataan, 16 untuk WHERE klausul, 4 untuk 'sa', dan 2 untuk tanda kutip yang dikelilingi oleh QUOTENAME(@loginname): 200 - 26 - 16 - 4 - 2 = 154. Tapi, karena @new dinyatakan sebagai sysname, variabel ini hanya dapat menampung 128 karakter. Kita dapat mengatasinya dengan meneruskan beberapa tanda kutip tunggal di @new.

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variable.
DECLARE @command VARCHAR(200)

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users SET password=' + QUOTENAME(@new, '''')
    + ' WHERE username=' + QUOTENAME(@loginname, '''') 
    + ' AND password=' + QUOTENAME(@old, '''')

-- Execute the command.
EXEC (@command);
GO

Jika penyerang meneruskan 154 karakter ke dalam buffer 128 karakter, penyerang dapat mengatur kata sandi baru tanpa sa mengetahui kata sandi lama.

EXEC sp_MySetPassword 'sa',
    'dummy',
    '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012'''''''''''''''''''''''''''''''''''''''''''''''''''

Untuk alasan ini, Anda harus menggunakan buffer besar untuk variabel perintah atau langsung menjalankan Transact-SQL dinamis di dalam EXECUTE pernyataan.

Pemotongan saat QUOTENAME(@variable, '''') dan REPLACE() digunakan

String yang dikembalikan oleh QUOTENAME() dan REPLACE() dipotong secara diam-diam jika melebihi spasi yang dialokasikan. Prosedur tersimpan yang dibuat dalam contoh berikut menunjukkan apa yang bisa terjadi.

Dalam contoh ini, data yang disimpan dalam variabel sementara dipotong, karena ukuran @loginbuffer , @oldpassword, dan @newpassword hanya 128 karakter, tetapi QUOTENAME() dapat mengembalikan hingga 258 karakter. Jika @new berisi 128 karakter, maka @newpassword bisa menjadi 123... n, di mana n adalah karakter ke-127. Karena string yang dikembalikan oleh QUOTENAME() dipotong, string dapat dibuat agar terlihat seperti pernyataan berikut:

UPDATE Users SET password ='1234...[127] WHERE username=' -- other stuff here

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variables.
DECLARE @login SYSNAME;
DECLARE @newpassword SYSNAME;
DECLARE @oldpassword SYSNAME;
DECLARE @command VARCHAR(2000);

SET @login = QUOTENAME(@loginname, '''');
SET @oldpassword = QUOTENAME(@old, '''');
SET @newpassword = QUOTENAME(@new, '''');

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users set password = ' + @newpassword
    + ' WHERE username = ' + @login
    + ' AND password = ' + @oldpassword;

-- Execute the command.
EXEC (@command);
GO

Oleh karena itu, pernyataan berikut menetapkan kata sandi semua pengguna ke nilai yang diteruskan dalam kode sebelumnya.

EXEC sp_MyProc '--', 'dummy', '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678'

Anda dapat memaksa pemotongan string dengan melebihi ruang buffer yang dialokasikan saat Anda menggunakan REPLACE(). Prosedur tersimpan yang dibuat dalam contoh berikut menunjukkan apa yang bisa terjadi.

Dalam contoh ini, data dipotong karena buffer yang dialokasikan untuk @login, @oldpassword dan @newpassword hanya dapat menampung 128 karakter, tetapi QUOTENAME() dapat mengembalikan hingga 258 karakter. Jika @new berisi 128 karakter, @newpassword bisa berupa '123...n', di mana n adalah karakter ke-127. Karena string yang dikembalikan oleh QUOTENAME() dipotong, string dapat dibuat agar terlihat seperti pernyataan berikut:

UPDATE Users SET password='1234...[127] WHERE username=' -- other stuff here

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variables.
DECLARE @login SYSNAME;
DECLARE @newpassword SYSNAME;
DECLARE @oldpassword SYSNAME;
DECLARE @command VARCHAR(2000);

SET @login = REPLACE(@loginname, '''', '''''');
SET @oldpassword = REPLACE(@old, '''', '''''');
SET @newpassword = REPLACE(@new, '''', '''''');

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users SET password = '''
    + @newpassword + ''' WHERE username = '''
    + @login + ''' AND password = ''' + @oldpassword + '''';

-- Execute the command.
EXEC (@command);
GO

Seperti halnya QUOTENAME(), pemotongan string dengan REPLACE() dapat dihindari dengan mendeklarasikan variabel sementara yang cukup besar untuk semua kasus. Jika memungkinkan, Anda harus memanggil QUOTENAME() atau REPLACE() langsung di dalam Transact-SQL dinamis. Jika tidak, Anda dapat menghitung ukuran buffer yang diperlukan sebagai berikut. Untuk @outbuffer = QUOTENAME(@input), ukuran @outbuffer harus 2 * (len(@input) + 1). Saat Anda menggunakan REPLACE() dan menggandakan tanda kutip, seperti dalam contoh sebelumnya, buffer 2 * len(@input) sudah cukup.

Perhitungan berikut mencakup semua kasus:

WHILE LEN(@find_string) > 0, required buffer size =
    ROUND(LEN(@input) / LEN(@find_string), 0)
        * LEN(@new_string) + (LEN(@input) % LEN(@find_string))

Pemotongan ketika QUOTENAME(@variable, ']') digunakan

Pemotongan dapat terjadi ketika nama Mesin Database yang dapat diamankan diteruskan ke pernyataan yang menggunakan formulir QUOTENAME(@variable, ']'). Diagram berikut menunjukkan skenario ini.

Dalam contoh ini, @objectname harus mengizinkan 2 * 258 + 1 karakter.

CREATE PROCEDURE sp_MyProc
    @schemaname SYSNAME,
    @tablename SYSNAME
AS
-- Declare a variable as sysname. The variable will be 128 characters.
DECLARE @objectname SYSNAME;

SET @objectname = QUOTENAME(@schemaname) + '.' + QUOTENAME(@tablename);
    -- Do some operations.
GO

Saat Anda menggabungkan nilai jenis sysname, Anda harus menggunakan variabel sementara yang cukup besar untuk menahan maksimum 128 karakter per nilai. Jika memungkinkan, hubungi QUOTENAME() langsung di dalam Transact-SQL dinamis. Jika tidak, Anda dapat menghitung ukuran buffer yang diperlukan seperti yang dijelaskan di bagian sebelumnya.