Injeksi SQL
Berlaku untuk: SQL Server Azure SQL Database Azure SQL Managed Instance Azure Synapse Analytics Analytics Platform 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
melaluiCOM8
, ,CONFIG$
CON
,LPT1
melaluiLPT8
, ,NUL
, danPRN
.
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
, , EXEC
atau 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 @login
buffer , @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.