Що таке SQL-ін'єкція та як її запобігти?
Практичний розбір атак SQL-ін'єкції, реальні приклади та конкретні способи їх зупинення в production-коді.
SQL-ін'єкція існує з кінця 1990-х років і досі є одним із найпоширеніших способів компрометації веб-додатків. Вразливість проста: зловмисник надсилає введення, яке інтерпретується як код SQL замість звичайних даних, і база даних виконує те, чого не мала робити.
Як атака насправді працює
Представте форму входу, яка будує запит так у PHP:
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
Ящо додаток не очищує введення, зловмисник вводить admin' -- у поле username. Запит стає:
SELECT * FROM users WHERE username = 'admin' --' AND password = ''
-- коментує решту рядка, тому перевірка пароля ніколи не відбувається. Це класичний обхід автентифікації. Зловмисники також використовують UNION SELECT для вилучення даних з інших таблиць або складають запити з крапкою з комою для запуску чогось деструктивного як DROP TABLE users;, якщо драйвер допускає кілька виразів.
Також існує сліпа SQL-ін'єкція, коли додаток не показує результати запиту прямо. Зловмисники виводять інформацію через часові затримки (SLEEP(5) в MySQL) або boolean-відповіді (сторінка поводиться інакше для умови true vs. false). Такі інструменти як sqlmap автоматизують цей вид екстракції, коли виявлена вразлива параметр.
Чому конкатенація рядків — корінь проблеми
Кожен варіант цієї атаки зводиться до однієї речі: змішування коду та даних в одному рядку. База даних не може розрізнити легітимне значення та введений синтаксис, тому що вони приходять одним каналом. Екранування лапок допомагає в деяких випадках, але це крихке рішення — різні кодування, second-order injection (дані збережені раз, потім небезпечно перевикористані пізніше) та специфічні особливості драйвера — усе це створює шляхи для обходу. Екранування — це латка, не виправлення.
Параметризовані запити — справжнє виправлення
Виправлення — розділити код SQL та дані користувача на рівні драйвера, використовуючи параметризовані запити (також звані підготовленими виразами). База даних отримує структуру запиту спочатку, потім прив'язує значення пізніше, тому введення користувача ніколи не може змінити значення запиту.
У Python з psycopg2:
cur.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))
У Node.js з mysql2:
connection.execute('SELECT * FROM users WHERE username = ? AND password = ?', [username, password]);
У Java з JDBC:
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE username = ? AND password = ?");
stmt.setString(1, username);
stmt.setString(2, password);
Звернігьте увагу на закономірність: заповідники (%s, ?) тримають місце для даних, а фактичні значення передаються окремо. Жодної конкатенації рядків, жодного ручного екранування не потрібно. Це працює для переважної більшості запитів, які ви напишете в звичайному додатку.
Як щодо динамічних таблиць або назв стовпців?
Параметризація обробляє значення, але не ідентифікатори — ви не можете прив'язати назву таблиці як параметр. Якщо ваш додаток потребує динамічного вибору таблиці (рідко, і часто це недолік дизайну), укладіть дозволені значення у hardcoded список замість того, щоб довіряти введенню користувача безпосередньо:
allowed_tables = {'orders', 'invoices', 'customers'}
if table_name not in allowed_tables:
raise ValueError("Invalid table")
Ніколи не будуйте назви ідентифікаторів через форматування рядків із введення користувача, навіть з екрануванням.
Багатошарові захисти поза самим запитом
Параметризовані запити — основний контроль, але дещо ще важливо:
- Найменший привілей на обліку бази даних. Користувач БД додатку не повинен мати
DROP,ALTERабо доступ до непов'язаних схем. Якщо ін'єкція все ж пройде, обмежені привілеї обмежують шкоду. - ORM допомагають за замовчуванням. Django ORM, SQLAlchemy та Hibernate всі параметризують запити автоматично, коли ви використовуєте їхні стандартні методи побудови запитів. Ризик повертається, коли розробники переходять на raw SQL або використовують
.extra()/text()виклики з інтерполяцією рядків — тому аудитуйте ці місця конкретно. - Валідація введення — вторинний шар, не заміна. Перевірка того, що поле email виглядає як email, це гарна практика, але це не зупиняє ін'єкцію саму по собі — зловмисники знаходять творчі payload, які все ж проходять слабку валідацію.
- WAF може виловити відомі шаблони атак, але це шар виявлення, не виправлення для базового коду.
Тестування вашого коду на цю вразливість
Пропустіть ваші запити через статичний інструмент аналізу (Bandit для Python, Semgrep з наборами правил SQL injection) як частину CI. Для ручного тестування спробуйте ін'єкцію одинарної лапки (') у кожне поле введення та спостерігайте за SQL-повідомленнями про помилки, витікаючими у відповідь — це часто перший знак того, що запит не параметризований.
Якщо ви хочете глибше вивчити це, трек Web Security Korra Studio охоплює ін'єкцію поряд з XSS та обходом автентифікації, а сегменти Databases пройдять через закономірності дизайну запитів, які уникають цього класу вразливостей цілком.
Написано з допомогою штучного інтелекту, перевірено та опубліковано Мішалем Пільчем (CISSP), Korra Studio.
Це одна замітка з бази знань Korra Studio — платформа поєднує кожну тему з 1-на-1 менторингом.
Початок безплатноarrow_forward