Що таке ін’єкція SQL і як її запобігти в додатках PHP?

Ви впевнені, що ваша база даних SQL надійно захищена від несанкціонованого втручання? SQL-ін’єкції можуть довести зворотне!

Так, ми говоримо про серйозну загрозу, а не про банальні “заходи безпеки” чи “запобігання несанкціонованому доступу”. SQL-ін’єкція – це настільки старий трюк, що кожен розробник знає про його небезпеку та методи захисту. Проте, навіть професіонали іноді припускаються помилок, і наслідки можуть бути руйнівними.

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

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

Суть SQL-ін’єкції криється у її назві: SQL + ін’єкція. Слово “ін’єкція” тут означає “введення”, а не медичну процедуру. У поєднанні ці слова описують процес впровадження SQL-коду у веб-застосунок.

Впровадження SQL у веб-застосунок. . . Хммм… Чи не цим ми займаємося постійно? Так, але ми не хочемо, щоб зловмисники керували нашою базою даних. Розглянемо це на прикладі.

Уявімо, що ви створюєте веб-сайт на PHP для місцевого інтернет-магазину. Ви вирішили додати форму зворотного зв’язку:

Файл `send_message.php` зберігає дані у базі даних, щоб власники магазину могли читати повідомлення користувачів. Код може виглядати так:

<?php

$name = $_POST['name'];
$message = $_POST['message'];

// Перевірка, чи користувач вже має повідомлення
mysqli_query($conn, "SELECT * from messages where name = $name");

// Інший код

Отже, ви спочатку намагаєтесь перевірити, чи має користувач непрочитане повідомлення. Запит `SELECT * from messages where name = $name` здається простим, чи не так?

НІ! Це велика помилка!

З необережності ми відкрили шлях до руйнування нашої бази даних. Для здійснення SQL-ін’єкції зловмиснику потрібні наступні умови:

  • Застосунок використовує базу даних SQL (практично кожна сучасна програма)
  • З’єднання з базою даних має права на “редагування” та “видалення”
  • Можна вгадати назви важливих таблиць

Третій пункт означає, що зловмисник, знаючи, що ви керуєте інтернет-магазином, припускає, що дані замовлень зберігаються в таблиці `orders`. З цією інформацією зловмисник може ввести наступне як своє ім’я:

Джо; TRUNCATE orders;? Так, ось так! Подивимось, що отримаємо у запиті PHP:

`SELECT * FROM messages WHERE name = Joe; TRUNCATE orders;`

Перша частина запиту містить помилку синтаксису (немає лапок навколо “Джо”), але крапка з комою змушує MySQL почати обробку нового запиту: `TRUNCATE orders`. Таким чином, одним махом вся історія замовлень зникає!

Тепер, коли ви розумієте принцип роботи SQL-ін’єкції, розглянемо способи її зупинити. Для успішної атаки SQL-ін’єкцією необхідні дві умови:

  • Сценарій PHP повинен мати права на зміну/видалення бази даних. Це стосується більшості програм, і ви не можете обмежити доступ лише на читання. Навіть якщо ми відмовимо в доступі до модифікацій, SQL-ін’єкція може дозволити переглядати всю базу даних, включно з конфіденційними даними. Тобто, зниження рівня доступу до бази даних не є ефективним способом захисту.
  • Обробка даних, введених користувачем. SQL-ін’єкція можлива лише тоді, коли ви обробляєте дані, введені користувачем. Звісно, неможливо відмовитися від вводу даних лише через побоювання SQL-ін’єкції.
  • Запобігання SQL-ін’єкції в PHP

    Оскільки з’єднання з базою даних, запити та введення користувача є необхідними елементами, як запобігти SQL-ін’єкції? На щастя, є два простих способи: 1) фільтрувати введені користувачем дані та 2) використовувати підготовлені запити.

    Фільтрація даних, введених користувачем

    Якщо ви використовуєте стару версію PHP (5.5 або нижче), варто пропускати всі введені користувачем дані через функцію `mysql_real_escape_string()`. Вона видаляє спеціальні символи з рядка, щоб вони втрачали своє значення при використанні у базі даних.

    Наприклад, якщо є рядок на кшталт `I’m a string`, зловмисник може використати символ одинарної лапки (‘), щоб змінити запит до бази даних та здійснити SQL-ін’єкцію. Функція `mysql_real_escape_string()` перетворить цей рядок на `I\’m a string`, додаючи зворотний слеш перед лапкою. У результаті весь рядок буде сприйнятий базою даних як звичайний текст, а не як частина запиту.

    Цей підхід має недолік: це застаріла техніка, яка використовується зі старими методами доступу до бази даних у PHP. Починаючи з PHP 7, ця функція більше не існує, що підводить нас до наступного рішення.

    Використання підготовлених запитів

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

    <?php
    $servername = "localhost";
    $username = "username";
    $password = "password";
    $dbname = "myDB";
    
    // Створення з'єднання
    $conn = new mysqli($servername, $username, $password, $dbname);
    
    // Перевірка з'єднання
    if ($conn->connect_error) {
        die("Connection failed: " . $conn->connect_error);
    }
    
    // Підготовка та прив'язка
    $stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");
    $stmt->bind_param("sss", $firstname, $lastname, $email);
    
    // Встановлення параметрів та виконання
    $firstname = "John";
    $lastname = "Doe";
    $email = "[email protected]";
    $stmt->execute();
    
    $firstname = "Mary";
    $lastname = "Moe";
    $email = "[email protected]";
    $stmt->execute();
    
    $firstname = "Julie";
    $lastname = "Dooley";
    $email = "[email protected]";
    $stmt->execute();
    
    echo "Нові записи успішно створено";
    
    $stmt->close();
    $conn->close();
    ?>
    

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

    Для тих, хто вже знайомий з розширенням PDO PHP і використовує його для створення підготовлених запитів, є невелика порада.

    Увага: обережно налаштовуйте PDO

    Використовуючи PDO для доступу до бази даних, ми можемо отримати хибне відчуття безпеки. “Я використовую PDO, мені більше ні про що не потрібно думати” – типова думка. PDO (або підготовлені запити MySQLi) достатньо для запобігання SQL-ін’єкціям, але потрібно бути обережним під час налаштування. Звичайне копіювання коду з підручників може призвести до скасування всього захисту:

    $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

    Цей параметр наказує PDO емулювати підготовлені запити, а не використовувати реальні можливості бази даних. PHP надсилає звичайні рядки запиту до бази даних, хоча ваш код виглядає так, ніби створює підготовлені запити. Тобто, ви все ще вразливі до SQL-ін’єкцій.

    Рішення просте: переконайтеся, що емуляція вимкнена.

    $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

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

    Запобігання за допомогою WAF

    Чи знаєте ви, що веб-додатки можна захистити від SQL-ін’єкцій за допомогою WAF (брандмауера веб-додатків)?

    WAF захищає не тільки від SQL-ін’єкцій, але й від багатьох інших вразливостей рівня 7, таких як міжсайтовий скриптинг, проблеми з автентифікацією, підробка міжсайтових запитів, витік даних тощо. Можна використовувати автономні рішення, як Mod Security, або хмарні сервіси.

    SQL-ін’єкції та сучасні PHP-фреймворки

    SQL-ін’єкція настільки поширена, проста, неприємна і небезпечна, що всі сучасні PHP-фреймворки мають вбудовані засоби захисту. У WordPress, наприклад, є функція `$wpdb->prepare()`, а фреймворки MVC роблять всю брудну роботу за вас. У WordPress потрібно явно підготувати запит, але це особливість WordPress. 🙂

    З іншого боку, сучасні веб-розробники часто не думають про SQL-ін’єкції і навіть не підозрюють про цю загрозу. Навіть якщо залишається один бекдор у їх програмі (параметр запиту `$_GET` та старі звички використовувати необроблені запити), наслідки можуть бути катастрофічними. Тому важливо виділити час для глибшого вивчення основ.

    Висновок

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