۱۳۸۸/۰۸/۱۵

امنيت در LINQ to SQL


سؤال: LINQ to SQL تا چه ميزان در برابر حملات تزريق SQL امن است؟
جواب كوتاه: بسيار زياد!

توضيحات:
string query = @"SELECT * FROM USER_PROFILE
WHERE LOGIN_ID = '"+loginId+@"' AND PASSWORD = '"+password+@"'";
گاهي از اوقات هر چقدر هم در مورد خطرات كوئري‌هايي از نوع فوق مقاله نوشته شود كافي نيست و باز هم شاهد اين نوع جمع زدن‌ها و نوشتن كوئري‌هايي به شدت آسيب پذير در حالت استفاده از ADO.Net كلاسيك هستيم. مثال فوق يك نمونه كلاسيك از نمايش آسيب پذيري در مورد تزريق اس كيوال است. يا نمونه‌ي بسيار متداول ديگري از اين دست كه با ورودي خطرناك مي‌تواند تا نمايش كليه اطلاعات تمامي جداول موجود هم پيش برود:
protected void btnSearch_Click(object sender, EventArgs e)
{
String cmd = @"SELECT [CustomerID], [CompanyName], [ContactName]
FROM [Customers] WHERE CompanyName ='" + txtCompanyName.Text
+ @"'";

SqlDataSource1.SelectCommand = cmd;

GridView1.Visible = true;
}
در اينجا فقط كافي است مهاجم با تزريق عبارت SQL مورد نظر خود، كوئري اوليه را كاملا غيرمعتبر كرده و از يك جدول ديگر در سيستم كوئري تهيه كند!
راه حلي كه براي مقابله با آن در دات نت ارائه شده نوشتن كوئري‌هاي پارامتري است و در اين حالت كار encoding اطلاعات ورودي به صورت خودكار توسط فريم ورك مورد استفاده انجام خواهد شد؛ همچنين براي مثال اس كيوال سرور، execution plan اين نوع كوئري‌هاي پارامتري را همانند رويه‌هاي ذخيره شده، كش كرده و در دفعات آتي فراخواني آن‌ها به شدت سريعتر عمل خواهد كرد. براي مثال:
SqlCommand cmd = new SqlCommand("SELECT UserID FROM Users WHERE UserName=@UserName AND Password=@Password");
cmd.Parameters.Add(new SqlParameter("@UserName", System.Data.SqlDbType.NVarChar, 255, UserName));
cmd.Parameters.Add(new SqlParameter("@Password", System.Data.SqlDbType.NVarChar, 255, Password));
dr = cmd.ExecuteReader();
if (dr.Read()) userId = dr.GetInt32(dr.GetOrdinal("UserID"));
زمانيكه از كوئري پارامتري استفاده شود، مقدار پارامتر، هيچگاه فرصت و قدرت اجرا پيدا نمي‌كند. در اين حالت صرفا به آن به عنوان يك مقدار معمولي نگاه خواهد شد و نه جزء قابل تغيير بدنه كوئري وارد شده كه در حالت جمع زدن رشته‌ها همانند اولين كوئري معرفي شده، تا حد انحراف كوئري به يك كوئري دلخواه مهاجم قابل تغيير است.

اما در مورد LINQ to SQL چطور؟
اين سيستم به صورت پيش فرض طوري طراحي شده است كه تمام كوئري‌هاي SQL نهايي حاصل از كوئري‌هاي LINQ نوشته شده توسط آن، پارامتري هستند. به عبارت ديگر اين سيستم به صورت پيش فرض براي افرادي كه داراي حداقل اطلاعات امنيتي هستند به شدت امنيت بالايي را به همراه خواهد آورد.
براي مثال كوئري LINQ زير را در نظر بگيريد:
var products = from p in db.products
where p.description.StartsWith(_txtSearch.Text)
select new
{
p.description,
p.price,
p.stock

};
اكنون فرض كنيد كاربر به دنبال كلمه sony باشد، آنچه كه بر روي اس كيوال سرور اجرا خواهد شد، دستور زير است (ترجمه نهايي كوئري فوق به زبان T-SQL) :
exec sp_executesql N'SELECT [t0].[description], [t0].[price], [t0].[stock]
FROM [dbo].[products] AS [t0]
WHERE [t0].[description] LIKE @p0',N'@p0 varchar(5)',@p0='sony%'
براي لاگ كردن اين عبارات SQL يا مي‌توان از SQL profiler استفاده نمود و يا خاصيت log زمينه مورد استفاده را بايد مقدار دهي كرد:
 db.Log = Console.Out;
و يا مي‌توان بر روي كوئري مورد نظر در VS.Net يك break point قرار داد و سپس از debug visualizer مخصوص آن استفاده نمود.

همانطور كه ملاحظه مي‌كنيد، كوئري نهايي توليد شده پارامتري است و در صورت ورود اطلاعات خطرناك در پارامتر p0 ، هيچ اتفاق خاصي نخواهد افتاد و صرفا ركوردي بازگشت داده نمي‌شود.

و يا همان مثال كلاسيك اعتبار سنجي كاربر را در نظر بگيريد:
public bool Validate(string loginId, string password)
{
DataClassesDataContext db = new DataClassesDataContext();

var validUsers = from user in db.USER_PROFILEs
where user.LOGIN_ID == loginId
&& user.PASSWORD == password
select user;

if (validUsers.Count() > 0) return true;
else return false;
}
كوئري نهايي T-SQL توليد شده توسط اين ORM از كوئري LINQ فوق به شكل زير است:
SELECT [t0].[LOGIN_ID], [t0].[PASSWORD]
FROM [dbo].[USER_PROFILE] AS [t0]
WHERE ([t0].[LOGIN_ID] = @p0) AND ([t0].[PASSWORD] = @p1)
و اين كوئري پارامتري نيز در برابر حملات تزريق اس كيوال امن است.

تذكر مهم هنگام استفاده از سيستم LINQ to SQL :

اگر با استفاده از LINQ to SQL مجددا به روش قديمي اجراي مستقيم كوئري‌هاي SQL خود همانند مثال زير روي بياوريد (اين امكان نيز وجود دارد)، نتيجه اين نوع كوئري‌هاي حاصل از جمع زدن رشته‌ها، پارامتري "نبوده" و مستعد به تزريق اس كيوال هستند:
string sql = "select * from Trade where DealMember='" + this.txtParams.Text + "'";
var trades = driveHax.ExecuteQuery<Trade>(sql);
در اينجا بايد در نظر داشت كه اگر شخصي مجددا بخواهد از اين نوع روش‌هاي كلاسيك استفاده كند شايد همان ADO.Net كلاسيك براي او كافي باشد و نيازي به تحميل سربار يك ORM را به سيستم نداشته باشد. در اين حالت برنامه از type safety كوئري‌هاي LINQ نيز محروم شده و يك لايه بررسي مقادير و پارامترها را توسط كامپايلر نيز از دست خواهد داد.

اما روش صحيحي نيز در مورد بكارگيري متد ExecuteQuery وجود دارد. استفاده از اين متد به شكل زير مشكل را حل خواهد كرد:
IEnumerable<Customer> results = db.ExecuteQuery<Customer>(
"SELECT contactname FROM customers WHERE city = {0}", "Tehran");
در اين حالت، پارامترهاي بكارگرفته شده (همان {0} ذكر شده در كوئري) به صورت خودكار به پارامترهاي T-SQL ترجمه خواهند شد و مشكل تزريق اس كيوال برطرف خواهد شد (به عبارت ديگر استفاده از +، علامت مستعد بودن به تزريق اس كيوال است و بر عكس).

Vote on iDevCenter