Як запобігти SQL-ін’єкції в C#

SQL-ін’єкція залишається серйозною загрозою для сучасних вебдодатків. У цьому посібнику показано, як розробники C# можуть запобігти ін’єкціям за допомогою безпечних практик написання коду та перевірити безпеку вебсайтів за допомогою DAST.

Що таке SQL-ін’єкція?

SQL-ін’єкція (SQLi) відбувається, коли зловмисники впроваджують шкідливий SQL-код у запити додатка, використовуючи неправильну обробку вхідних даних користувача.

Це може дозволити несанкціонований доступ, маніпулювання даними або навіть повну компрометацію бази даних.

Приклад вразливого коду C#

string query = "SELECT * FROM Users WHERE Username = '" + username + "' AND Password = '" + password + "'";

За допомогою цієї логіки автентифікації додаток очікує, що база даних поверне непорожній набір результатів, якщо комбінація імені користувача та пароля існує в таблиці Users. Якщо зловмисник введе admin’ OR ‘1’=’1 у поле або параметр імені користувача, сформований SQL-запит виглядатиме приблизно так:

SELECT * FROM Users WHERE Username = 'admin' OR '1'='1' AND Password = ''

Якщо база даних виконує такий запит, він завжди повертатиме результат, оскільки умова 1=1 є завжди істинною. Це дозволяє зловмиснику обійти механізм автентифікації.

Найкращі практики запобігання SQL-ін’єкціям у C#

Захист від SQL-ін’єкцій полягає в тому, щоб не дозволити зловмисникам вставляти або змінювати запити до бази даних, створені та надіслані додатком.

Параметризовані запити

Параметризовані запити відокремлюють логіку SQL від даних користувача, щоб запобігти інтерпретації вхідних даних як виконуваного SQL. Вхідні дані, керовані користувачем, автоматично очищуються та кодуються середовищем виконання, щоб переконатися, що їх можна безпечно виконати як частину запиту.

Ось спрощений приклад безпечної перевірки імені користувача та пароля в C#:

using (SqlConnection connection = new SqlConnection(connectionString))
{
    string query = "SELECT * FROM Users WHERE Username = @Username AND Password = @Password";
    SqlCommand command = new SqlCommand(query, connection);
    command.Parameters.AddWithValue("@Username", username);
    command.Parameters.AddWithValue("@Password", password);
    connection.Open();
    SqlDataReader reader = command.ExecuteReader();
}

Важливо: у реальному коді продакшну слід працювати з хешами паролів, а не використовувати відкритий текст, як у цьому спрощеному прикладі.

Щоб додати перевірку вхідних даних на цьому етапі, можна використовувати Parameters.Add() замість Parameters.AddWithValue() та вказати очікуваний тип даних і довжину під час створення нового об’єкта SqlParameter:

command.Parameters.Add(new SqlParameter("@Username", SqlDbType.NVarChar, 50) { Value = username });

Фреймворки ORM

Хоча об’єктно-реляційної проєкції (object-relational mapping, ORM), наприклад, Entity Framework, не розроблені спеціально для безпеки, проте вони можуть допомогти запобігти ін’єкціям, оскільки ORM автоматично обробляють параметризацію запитів:

var user = context.Users.FirstOrDefault(u => u.Username == username && u.Password == password);

Правильне використання ORM може не лише зменшити ризик SQL-ін’єкцій, але й полегшити написання безпечнішого коду загалом.

Додатковий захист

Жодна з цих практик сама по собі не запобіжить ін’єкціям, тому їх завжди слід застосовувати в поєднанні:

  • Параметризовані збережені процедури

Збережені процедури можуть бути хорошим способом зменшення ризику ін’єкцій, визначаючи та зберігаючи запит у самій базі даних, окремо від обробки вхідних даних. Однак, щоб вони були безпечними, їх все одно потрібно правильно параметризувати – динамічно згенерована збережена процедура може бути такою ж вразливою, як і запит, побудований з об’єднаних рядків.

  • Перевірка вхідних даних

Варто застосовувати відповідні обмеження формату для вхідних даних, щоб звузити можливості ін’єкцій (наприклад, забороняти рядкові вхідні дані, де очікується лише ціле число).

  • Очищення вхідних даних

Слід перевіряти вхідні дані на відповідність очікуваним значенням, де це можливо. А також фільтрувати будь-які очевидні спеціальні символи, завжди пам’ятаючи, що будь-який фільтр можна обійти, і йому не слід йому довіряти як єдиному захисту.

  • Принцип найменших привілеїв

Потрібно використовувати обмежені облікові записи бази даних, щоб знизити вплив будь-якої успішної атаки. Це стосується як облікового запису користувача бази даних, до якого отримує доступ додаток, так і користувача ОС процесу бази даних на сервері.

Чому ручної санітизації та фільтрації недостатньо

Щоб запобігти ін’єкціям, ніколи не достатньо просто видалити спеціальні символи SQL з вхідних даних, як у наступному прикладі, адже цей підхід легко обійти:

public static string SanitizeSqlString(string input)
{
    return input?.Replace("'", "").Replace("\"", "").Replace(";", "");
}

Так само, перевірка на основі регулярних виразів фільтрує лише шаблони вхідних даних, і хоча вона може покращити їх якість та взаємодію з користувачем (наприклад, виявляючи деякі помилки друку перед надсиланням), вона не запобігатиме виконанню запитів і не повинна розглядатися як окремий захід безпеки:

if (Regex.IsMatch(username, @"^[a-zA-Z0-9]+$")) { /* not sufficient */ }

В ідеалі, слід використовувати вбудовані функції санітизації та перевірки, що надаються мовою або фреймворком, аби покращити безпеку коду.

Як убезпечити C# MVC та LINQ від SQL-ін’єкцій

В ASP.NET MVC рекомендується використовувати валідацію моделі для контролю формату, а також застосовувати параметризовані запити або засоби доступу до бази даних на основі ORM (наприклад, Entity Framework). Це дозволяє значно знизити ризик SQL-ін’єкцій, забезпечуючи належну обробку введених користувачем значень.

[HttpPost]
public ActionResult Login(string username, string password)
{
    if (ModelState.IsValid)
    {
        using (SqlConnection connection = new SqlConnection(connectionString))
        {
            string query = "SELECT * FROM Users WHERE Username = @Username AND Password = @Password";
            SqlCommand command = new SqlCommand(query, connection);
            command.Parameters.AddWithValue("@Username", username);
            command.Parameters.AddWithValue("@Password", password);
            connection.Open();
            SqlDataReader reader = command.ExecuteReader();
        }
    }
    return View();
}

Під час використання LINQ не потрібно безпосередньо маніпулювати SQL, щоб визначити та виконати запит:

var users = context.Users.Where(u => u.Username == username);

У поєднанні з Entity Framework безпека покращується завдяки автоматичній параметризації (за умови уникання навмисного написання необробленого SQL).

Тестування безпеки

Незалежно від технологічного стека, систематичне та автоматизоване сканування вразливостей є обов’язковим.

DAST-платформа Invicti (раніше Netsparker) знаходить усі SQL-ін’єкції та інші вразливості, а також підтверджує їх існування. Щоб у цьому переконатися, ви можете безкоштовно протестувати Invicti, для цього зверніться до нас зручним для вас способом.

Підписатися на новини