Пренос параметара командном објекту¶
Када се говори о преносу параметара командном објекту, мисли се на начин на
који се вредности прослеђују у SQL упите или ускладиштене процедуре путем
SqlCommand објекта. Ово је кључно за: безбедност, јер се избегава могућност
SQL injection-а; за перформансе, јер је боља припрема и кеширање упита; и на
крају, за јасноћу и одвојеност логике и података.
SQL injection (SQLi) је један од најстаријих и најопаснијих облика сајбер напада, који је први пут документован крајем деведесетих година прошлог века. Овај напад омогућава хакерима да убаце злонамерне SQL команде у апликације које користе базе података, чиме могу да приступе осетљивим информацијама, да измене или униште податке. Компаније могу изгубити милионе због крађе података, као што је случај са нападом на Heartland Payment Systems 2008. године, где је компромитовано 130 милиона бројева кредитних картица. Поред финансијске штете, компаније могу претрпети трајна оштећења свог имиџа, репутације и кредибилитета. Можеш само да замислиш колико страшни могу бити сигурносни пропусти због којих се открију лични подаци и угрози приватност и безбедност људи. Иако је SQLi релативно лако спречити, он и даље представља велики ризик због своје једноставности и ефикасности, па је због тога ово изузетно важна лекција!
Како НЕ ТРЕБА слати упите командном објекту¶
У бази података Northwind постоји табела Customers која садржи податке о
купцима. Направио си апликацију која омогућава запосленима да претражују купце
на основу земље из које долазе.
Нека табела Customers изгледа овако:
CustomerID |
CompanyName |
City |
Country |
|---|---|---|---|
ALFKI |
Alfreds Futterkiste |
Berlin |
Germany |
ANATR |
Ana Trujillo Emparedados y helados |
México D.F. |
Mexico |
ANTON |
Antonio Moreno Taquería |
México D.F. |
Mexico |
AROUT |
Around the Horn |
London |
UK |
BERGS |
Berglunds snabbköp |
Luleå |
Sweden |
Програмер је направио форму за претрагу. Када корисник унесе назив земље и кликне на дугме, апликација ће приказати све купце из те земље. Кôд догађаја клика на дугме за претрагу изгледа овако:
private void btnSearch_Click(object sender, EventArgs e)
{
string country = txtCountry.Text;
string connString = "Data Source=LOCALHOST\\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=True";
string sqlUpit = $"SELECT CustomerID, CompanyName, City FROM Customers WHERE Country = '{country}'";
using (SqlConnection conn = new SqlConnection(connString))
{
conn.Open();
SqlCommand cmd = new SqlCommand(sqlUpit, conn);
using (SqlDataReader reader = cmd.ExecuteReader())
{
if (reader.HasRows)
{
// Prikaz rezultata na formi...
}
else
{
MessageBox.Show("Није пронађен ниједан купац из унете земље.");
}
}
}
}
Када корисник унесе назив земље (нпр. Germany), формира се стринг sqlUpit,
отвара се конекција ка бази и упит се прослеђује објекту SqlCommand на
извршавање. Ако постоје купци из те земље, reader ће имати редове и они ће
бити приказани. На први поглед, све делује исправно.
Али, ако овакву апликацију користи злонамерни корисник, он у пољу за унос земље може унети…
' OR 1 = 1 --
…што значи да би формирани стринг sqlUpit изгледао овако:
SELECT CustomerID, CompanyName, City FROM Customers WHERE Country = '' OR 1 = 1 --'
Иако не постоји земља са називом '' (празан стринг), израз 1 = 1 је увек
тачан. Због оператора OR, цео услов у WHERE клаузули постаје тачан за сваки
ред у табели. Део упита након -- третира се као коментар и игнорише се. Као
резултат, упит ће вратити све купце из табеле Customers, без обзира на земљу,
што може представљати озбиљан сигурносни пропуст и цурење података.
Ако упит може да садржи било какав унос корисника, никада немој да га формираш спајањем стрингова! Увек користи параметризоване упите.
Параметризовање упита методом AddWithValue¶
Да би избегао опасности од SQL injection рањивости апликације, потребно је да
користиш параметризоване упите. То значи да SQL упит не садржи директно
кориснички унос као део текста, већ се уместо тога користе параметри који се
додељују преко методе AddWithValue(). На тај начин се кориснички унос никада
не тумачи као део SQL синтаксе, већ искључиво као вредност. Ево како можеш исти
пример претраге написати на безбедан начин:
private void btnSearch_Click(object sender, EventArgs e)
{
string country = txtCountry.Text;
string connString = "Data Source=LOCALHOST\\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=True";
string sqlUpit = "SELECT CustomerID, CompanyName, City FROM Customers WHERE Country = @country";
using (SqlConnection conn = new SqlConnection(connString))
{
conn.Open();
SqlCommand cmd = new SqlCommand(sqlUpit, conn);
cmd.Parameters.AddWithValue("@country", country);
using (SqlDataReader reader = cmd.ExecuteReader())
{
if (reader.HasRows)
{
// Prikaz rezultata na formi...
}
else
{
MessageBox.Show("Није пронађен ниједан купац из унете земље.");
}
}
}
}
Уместо да у sqlUpit директно убацујеш вредност променљиве country, користиш
именовани параметар @country. Методом AddWithValue() се том параметру
додељује вредност коју је корисник унео. На овај начин, чак и ако неко покуша
да убаци SQL кôд (нпр. ' OR 1 = 1 --), тај унос ће бити третиран као обична
текстуална вредност (као да тражи земљу која се дословно зове ' OR 1 = 1 --),
а не као део упита. SQL сервер се брине да ови подаци не угрозе структуру
упита, чиме се у потпуности елиминише ризик од SQL injection-а.
Обрати пажњу на именовање параметара – ако користиш @country у упиту, мораш
тај исти идентификатор да користиш и у методи AddWithValue(). Назив параметра
мора увек да почне карактером @. У језику T-SQL параметри нису осетљиви на
велика/мала слова, али у програмском језику C# јесу, па је зато добра пракса да
будеш доследан приликом именовања идентификатора. О стиловима именовања
идентификатора
учио си у првом разреду.
Параметризовање упита методом Add¶
Иако је метода AddWithValue згодна за брзо писање кода, у пракси се
препоручује употреба методе Add, где се тачно наводи тип података и, ако је
потребно, величина параметра. Ово је посебно важно када радиш са текстуалним
пољима, јер погрешна претпоставка о типу или дужини може довести до неефикасних
упита или грешака у раду са базом.
Претходни пример написан помоћу методе Add изгледао би овако:
private void btnSearch_Click(object sender, EventArgs e)
{
string country = txtCountry.Text;
string connString = "Data Source=LOCALHOST\\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=True";
string sqlUpit = "SELECT CustomerID, CompanyName, City FROM Customers WHERE Country = @country";
using (SqlConnection conn = new SqlConnection(connString))
{
conn.Open();
SqlCommand cmd = new SqlCommand(sqlUpit, conn);
// Kolona Country u Northwind bazi je tipa nvarchar(15)
cmd.Parameters.Add("@country", SqlDbType.NVarChar, 15).Value = country;
using (SqlDataReader reader = cmd.ExecuteReader())
{
if (reader.HasRows)
{
// Prikaz rezultata na formi...
}
else
{
MessageBox.Show("Није пронађен ниједан купац из унете земље.");
}
}
}
}
Зашто је боље да користиш методу Add? У Northwind бази, колона Country је
дефинисана као NVarChar(15). Навођењем тачног типа (SqlDbType.NVarChar) и
величине (15), помажеш SQL Server-у да ефикасније креира и кешира план
извршења упита, што може побољшати перформансе апликације. Када се тип и
величина подударају, SQL Server не мора да ради имплицитне конверзије и може
боље да искористи постојеће индексе.
Сада је право време да се подсетиш
лекција о типовима података
у програмском језику C# и упоредиш их са онима које си користио у наставном
предмету Базе података у II и III разреду, односно са типовима података у SQL
Server Database Engine-у. SQL Server и .NET Framework засновани су на
различитим системима типова података. Иако изгледа да имају сличне типове (нпр.
System.Double у .NET Framework-у и float у SQL Server-у), њихова унутрашња
структура и прецизност често се разликују. Зато, када апликација чита или шаље
бројеве ка бази, .NET Framework алати, попут SqlDataReader-а, дају ти могућност
да бираш да ли желиш SQL Server тип (нпр. SqlDouble) или .NET Framework тип
(нпр. double). Приликом креирања параметара, можеш прецизно да кажеш који је
тип података тако што користиш DbType.Double или SqlDbType.Float, како не
би дошло до губитка података.
Комплетну табелу за мапирање типова SQL Server Database Engine-а и .NET Framework-а можеш погледати у прилогу са мапирањем типова.
На крају ове лекције, најважније је да запамтиш следеће:
Никада немој конструисати SQL упите спајањем стрингова са корисничким уносом. То је највећи сигурносни пропуст који можеш направити. Увек користи параметре за прослеђивање вредности у SQL упите. Тиме штитиш апликацију од SQL Injection напада.
Дај предност методи
Addу односу наAddWithValue. Експлицитно дефинисање типа и величине параметра побољшава перформансе и спречава потенцијалне грешке.
Правилно руковање параметрима није само техничка вештина, већ и основа професионалног и одговорног развоја софтвера.