در اکوسیستم .NET (شامل .NET Core، .NET 8 و نسخه ۹)، فریمورکهای توسعه و ابزارهای داده مانند Entity Framework Core امکانات قدرتمندی برای ایمنسازی لایه دسترسی به داده ارائه میدهند. با این حال، خطاهای برنامهنویسی، عدم درک صحیح از رفتارهای فریمورک، استفاده نادرست از Dynamic SQL و پیادهسازیهای دستی ناایمن میتوانند نفوذگران را مستقیماً به پایگاه داده وصل کنند.
حمله SQL Injection زمانی رخ میدهد که دادههای ورودی کاربر، بدون اعتبارسنجی یا پارامتریسازی، مستقیماً با دستورات SQL ترکیب (String Concatenation) و به دیتابیس ارسال شوند. مفسر SQL ورودیهای کاربر را به عنوان بخشی از دستور یا ساختار منطقی Query در نظر میگیرد.
+-------------------+ Unsafe Query +-------------------+
| Attacker Input | ---------------------> | Database Engine |
| ' OR '1'='1 | Interpreted as Code | (Data Exfiltration|
+-------------------+ | or Destruction) |
+-------------------+
انواع اصلی حملات SQLi
In-Band SQLi (Classic): نفوذگر پاسخ مستقیم دستور تزریق شده را در خروجی برنامه مشاهده میکند.
Inferential / Blind SQLi: برنامهکاربردی هیچ خروجی یا خطایی نشان نمیدهد، اما نفوذگر با ارسال پرسوجوهای شرطی منطق دیتابیس را کشف میکند.
Out-of-Band SQLi: نفوذگر دادهها را از طریق کانالهای دیگر (مانند درخواستهای DNS یا HTTP از سمت دیتابیس) استخراج میکند (متداول در SQL Server با xp_dirtree).
بسیاری از برنامهنویسان به اشتباه تصور میکنند که استفاده از .NET یا Entity Framework به خودی خود شیلد امنیتی کاملی در برابر SQLi ایجاد میکند. در ادامه چند سناریوی آسیبپذیر در برنامههای واقعی را بررسی میکنیم.
سناریوی ۱: اتصال ناایمن رشتهها در ADO.NET
// ❌ code بسیار آسیبپذیر در ADO.NET
public User GetUserUnsafe(string username, string password)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
// ترکیب مستقیم ورودیها با Query
string query = "SELECT * FROM Users WHERE Username = '" + username + "' AND Password = '" + password + "'";
using (var command = new SqlCommand(query, connection))
{
using (var reader = command.ExecuteReader())
{
if (reader.Read())
{
return MapUser(reader);
}
}
}
}
return null;
}
روش نفوذ:
اگر ورودی username برابر با ' OR '1'='1 باشد، Query نهایی به شکل زیر تفسیر شده و بدون نیاز به کلمه عبور، اولین کاربر (معمولاً مدیر سیستم) را بازیابی میکند:
SELECT * FROM Users WHERE Username = '' OR '1'='1' AND Password = '...'
سناریوی ۲: استفاده ناایمن از Raw SQL در EF Core
استفاده از متدهای Raw SQL در Entity Framework Core بدون پارامترهای نامگذاری شده بسیار خطرناک است.
// ❌ کد آسیبپذیر در EF Core با استفاده از Interpolated String ساده در FromSqlRaw
public List<Product> SearchProductsUnsafe(string searchTerm)
{
// استفاده از Interpolated String در FromSqlRaw خطرناک است!
string sql = $"SELECT * FROM Products WHERE Name LIKE '%{searchTerm}%'";
return _context.Products.FromSqlRaw(sql).ToList();
}
تکنیکهای عملی و متدولوژیهای پیشگیری
بررسی و اجرای تکتک تکنیکهای زیر برای تأمین امنیت لایه داده در برنامههای .NET الزامی است.
تکنیک ۱: پارامتریسازی کامل (Parameterized Queries / Prepared Statements)
اصلیترین و حیاتیترین مکانیزم امنیتی، جداسازی بخش «کد SQL» از «داده ورودی» است. زمانی که پرسوجو پارامتری میشود، دیتابیس ابتدا ساختار دستور را کامپایل میکند و ورودیها را فقط به عنوان «مقدار متغیر» پردازش خواهد کرد، نه دستور قابل اجرا.
اصلاح در ADO.NET:
// ✅ استفاده صحیح از Parameterized Query
public User GetUserSafe(string username, string password)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
string query = "SELECT * FROM Users WHERE Username = @Username AND PasswordHash = @PasswordHash";
using (var command = new SqlCommand(query, connection))
{
// تعریف صریح پارامترها و نوع داده آنها
command.Parameters.Add("@Username", SqlDbType.VarChar, 50).Value = username;
command.Parameters.Add("@PasswordHash", SqlDbType.VarChar, 256).Value = password;
using (var reader = command.ExecuteReader())
{
if (reader.Read())
{
return MapUser(reader);
}
}
}
}
return null;
}
تکنیک ۲: بهرهگیری صحیح از EF Core و LINQ
در LINQ تمامی پرسوجوها به صورت خودکار به Parameterized Queries تبدیل میشوند.
// ✅ استفاده امن از LINQ
public async Task<List<Product>> SearchProductsSafeAsync(string searchTerm)
{
return await _context.Products
.Where(p => p.Name.Contains(searchTerm))
.AsNoTracking()
.ToListAsync();
}
نحوه صحیح اجرای Raw SQL در EF Core:
اگر نیاز مبرم به نوشتن کدهای SQL دستی دارید، حتماً از متد FromSqlInterpolated یا پارامترهای SqlParameter در FromSqlRaw استفاده کنید.
// ✅ روش اول: استفاده از FromSqlInterpolated (EF Core به صورت خودکار آن را پارامتری میکند)
public List<Product> SearchProductsSafe1(string searchTerm)
{
return _context.Products
.FromSqlInterpolated($"SELECT * FROM Products WHERE Name LIKE '%' + {searchTerm} + '%'")
.ToList();
}
// ✅ روش دوم: استفاده از FromSqlRaw به همراه SqlParameter
public List<Product> SearchProductsSafe2(string searchTerm)
{
var param = new SqlParameter("@Search", $"%{searchTerm}%");
return _context.Products
.FromSqlRaw("SELECT * FROM Products WHERE Name LIKE @Search", param)
.ToList();
}
تکنیک ۳: ایمنسازی Stored Procedureها
استفاده از Stored Procedure به خودی خود تضمینکننده امنیت نیست! اگر درون خود Stored Procedure دستورات Dynamic SQL به صورت غیرایمن رشتهبندی شوند، آسیبپذیری همچنان وجود خواهد داشت.
❌ Stored Procedure ناایمن در SQL Server:
CREATE PROCEDURE dbo.UnsafeSearch
@Term NVARCHAR(50)
AS
BEGIN
DECLARE @SQL NVARCHAR(MAX);
-- ترکیب رشته داخل SQL Server
SET @SQL = 'SELECT * FROM Products WHERE Name LIKE ''%' + @Term + '%''';
EXEC sp_executesql @SQL;
END
✅ Stored Procedure ایمن:
CREATE PROCEDURE dbo.SafeSearch
@Term NVARCHAR(50)
AS
BEGIN
-- استفاده از sp_executesql با پارامترهای مشخص
DECLARE @SQL NVARCHAR(MAX);
SET @SQL = N'SELECT * FROM Products WHERE Name LIKE @InnerTerm';
DECLARE @Pattern NVARCHAR(52) = '%' + @Term + '%';
EXEC sp_executesql @SQL, N'@InnerTerm NVARCHAR(52)', @InnerTerm = @Pattern;
END
در فراخوانی Stored Procedure از سمت .NET نیز باید پارامترها را به درستی بفرستید:
public async Task<List<Product>> GetProductsByProcAsync(string term)
{
var termParam = new SqlParameter("@Term", term ?? (object)DBNull.Value);
return await _context.Products
.FromSqlRaw("EXEC dbo.SafeSearch @Term", termParam)
.ToListAsync();
}
تکنیک ۴: مدیریت Dynamic Sorting و Dynamic Column Selection
یکی از سناریوهایی که برنامهنویسان را به سمت ترکیب غیرایمن رشتهها سوق میدهد، مرتبسازی پویا (Dynamic OrderBy) بر اساس نام ستونی است که کاربر انتخاب کرده است. پارامترهای SQL فقط برای مقادیر (Values) کار میکنند، نه برای نام جداول یا ستونها!
❌ روش ناایمن:
// آسیبپذیر در برابر تزریق نام ستون
string sql = $"SELECT * FROM Products ORDER BY {userInputColumn}";
✅ روش ایمن اول: استفاده از Whitelist (لیست سفید)
public async Task<List<Product>> GetSortedProductsAsync(string sortByColumn, bool ascending)
{
// تعریف صریح لیست ستونهای مجاز
var allowedColumns = new Dictionary<string, Expression<Func<Product, object>>>(StringComparer.OrdinalIgnoreCase)
{
{ "name", p => p.Name },
{ "price", p => p.Price },
{ "createdat", p => p.CreatedAt }
};
if (!allowedColumns.ContainsKey(sortByColumn))
{
sortByColumn = "name"; // ستون پیشفرض در صورت ورودی غیرمجاز
}
var keySelector = allowedColumns[sortByColumn];
var query = _context.Products.AsQueryable();
query = ascending ? query.OrderBy(keySelector) : query.OrderByDescending(keySelector);
return await query.ToListAsync();
}
✅ روش ایمن دوم: استفاده از کتابخانه System.Linq.Dynamic.Core
اگر نیاز به مرتبسازی دینامیک پیچیده دارید، کتابخانه System.Linq.Dynamic.Core گزینه مناسبی است؛ اما همچنان باید ورودی کاربر با Whitelist سنجیده شود.
تکنیک ۵: اعتبارسنجی ورودیها (Input Validation) و Sanitation
گرچه اعتبارسنجی ورودی هرگز نباید جایگزین Parameterized Queries شود، اما به عنوان لایه دفاعی مکمل (Defense-in-Depth) بسیار ضروری است.
Strong Typing: تا حد امکان از انواع دادهای مشخص (مانند int, Guid, DateTime) به جای string استفاده کنید.
Regex Patterns: استفاده از Regular Expression برای محدود ساختن کاراکترها در ورودیهای متنی (مثلاً کد ملی، ایمیل، شماره تلفن).
FluentValidation: پیادهسازی قوانین سختگیرانه روی DTOها.
public class UserSearchRequestValidator : AbstractValidator<UserSearchRequest>
{
public UserSearchRequestValidator()
{
RuleFor(x => x.SearchQuery)
.MaximumLength(100)
.Matches(@"^[a-zA-Z0-9\s_\-]+$")
.WithMessage("Search term contains invalid characters.");
}
}
تکنیک ۶: پیکربندی امن دیتابیس و اصل حداقل دسترسی (Least Privilege)
امنیت نرمافزار منحصر به کد .NET نیست. حساب کاربری دیتابیس که برنامه با آن به SQL Server یا PostgreSQL متصل میشود نقش حیاتی دارد:
محدود کردن دسترسیها: اکانتی که برنامه با آن متصل میشود نباید دسترسی sysadmin یا db_owner داشته باشد.
تفکیک دسترسیها: اگر برنامه صرفاً نیاز به خواندن و نوشتن داده دارد، تنها نقشهای db_datareader و db_datawriter را تخصیص دهید.
جلوگیری از اجرای دستورات خطرناک: دسترسی به رویههای اجرایی سیستم مانند xp_cmdshell را کلاً در دیتابیس غیرفعال کنید.
-- غیرفعال کردن xp_cmdshell در SQL Server
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 0;
RECONFIGURE;
ابزارها و تستهای خودکار شناسایی SQL Injection
برای اطمینان از عدم وجود آسیبپذیری تزریق اسکیوال در کدهای .NET، استفاده از ابزارهای تست خودکار در خط لوله CI/CD ضروری است.
|
نوع ابزار |
نام ابزار |
کاربرد |
|
SAST (Static Analysis) |
SonarQube / Roslyn Security Guard |
تحلیل ایستا و بررسی کد منبع برای کشف ترکیب ناایمن رشتهها |
|
DAST (Dynamic Analysis) |
OWASP ZAP / Burp Suite |
اسکن دینامیک برنامههای در حال اجرای .NET |
|
Database PenTesting |
SQLMap |
تست نفوذ اتوماتیک برای ارزیابی میزان آسیبپذیری پایگاه داده |
چکلیست امنیتی مهندسان .NET
برای اطمینان از ایمن بودن لایه دسترسی داده در پروژههای .NET، همواره چکلیست زیر را بررسی کنید:
[ ] عدم استفاده از String Concatenation: از ترکیب مستقیم رشتهها با متغیرهای کاربر برای ساخت Query تحت هیچ شرایطی استفاده نشده است.
[ ] استفاده از LINQ یا Parameterized Query: تمامی درخواستها از طریق LINQ یا دستورات پارامتری شده اجرا میشوند.
[ ] بررسی کدهای Raw SQL: متدهای FromSqlRaw و ExecuteSqlRaw بررسی شده و در صورت نیاز با FromSqlInterpolated جایگزین شدهاند.
[ ] کنترل مرتبسازی پویا: ستونها و پارامترهای Sorting بر اساس یک Whitelist ولیدیت میشوند.
[ ] محدودسازی دسترسی دیتابیس: Connection String با اکانتی کار میکند که دارای دسترسی محدودی است.
[ ] اسکن ایستا (SAST): کدهای پروژه در مخزن با ابزارهایی مانند SonarQube آنالیز میشوند.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.