SQL-ін’єкція (SQLi) залишається однією з найсерйозніших і найпоширеніших загроз у веббезпеці. Для розробників PHP розуміння того, як працює ця вразливість, і як їй запобігти, є критично важливим для створення безпечних вебдодатків.
Що таке SQL-ін’єкція, і чому вона небезпечна?
SQL-ін’єкція відбувається, коли дані, надані користувачем, вбудовуються безпосередньо в SQL-запит без належних заходів безпеки. Шкідливі вхідні дані можуть змінити SQL-код, що призводить до несанкціонованого доступу, крадіжки даних або маніпуляцій з вмістом бази даних.
Приклад SQL-ін’єкції
Приклад наведено для розуміння спеціалістами з безпеки та розробки принципу роботи атаки, і в жодному разі не закликає до протиправних дій.
Нижче наведено вразливу реалізацію входу в PHP, де будь-які значення, які користувач надає для імені користувача та пароля, безпосередньо об’єднуються з SQL-запитом:
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $query);
Потім додаток виконує автентифікацію, перевіряючи, чи запит до бази даних повертає непорожній результат.
Важливо: в додатках у продакшні ніколи не слід надсилати, зберігати або порівнювати паролі у текстовому форматі – варто порівнювати лише хеші паролів.
Припускаючи, що зловмисник знає про існування дефолтного користувача під назвою admin, все, що йому потрібно зробити для входу, це ввести admin’ — для імені користувача та будь-яке значення для пароля (по суті будь-що, що прийме логіка програми). Як наслідок SQL-запит стає таким:
SELECT * FROM users WHERE username = 'admin' --' AND password = 'anything'
Символ — починає коментар, тому решта запиту ігнорується. База даних виконує:
SELECT * FROM users WHERE username = 'admin'
Поки існує цей користувач-адміністратор, завжди повертатиметься непорожній набір результатів, фактично повністю обходячи перевірку пароля та надаючи несанкціонований доступ до облікового запису адміністратора.
SQLi також може використовуватися зловмисниками для інших сценаріїв атак.
Чому екранування недостатньо?
Іноді старий код намагається використовувати mysqli_real_escape_string() для очищення вхідних даних як запобіжний захід SQL-ін’єкції:
$username = mysqli_real_escape_string($conn, $_GET['user']);
$query = "SELECT * FROM users WHERE name = '$username'";
Хоча це й зменшує ризик ін’єкцій, така практика не є надійною. Екранування саме по собі не може захистити від усіх варіантів SQL-ін’єкцій, особливо з неправильно налаштованими підключеннями до бази даних. Воно також нічого не робить зі структурними частинами запиту, як назви стовпців або таблиць.
Основи запобігання SQL-ін’єкції
Найкращий захист від SQL-ін’єкцій у PHP – це використання параметризованих запитів з підготовленими операторами.
Вони відокремлюють дані користувача від SQL-коду, гарантуючи, що будь-які спеціальні символи та ключові слова SQL у вхідних даних користувача не зможуть змінити структуру запиту.
Важливо: хоча в обох прикладах нижче для простоти використовуються жорстко закодовані облікові дані, у реальному застосунку слід завантажувати деталі підключення до бази даних зі змінних середовища (за допомогою getenv()) або безпечного джерела конфігурації, щоб уникнути розкриття конфіденційної інформації.
Використання PDO (PHP Data Objects)
PDO надає інтерфейс для доступу до баз даних. Він підтримує плейсхолдери з назвами та вбудовану обробку помилок.
Ось приклад запиту на користувачів, що відповідають заданому числовому статусу:
// Hardcoded credentials for illustration only
$pdo = new PDO("mysql:host=localhost;dbname=testdb", "user", "pass");
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = :name AND status = :status");
$stmt->bindParam(':name', $_GET['user'], PDO::PARAM_STR);
$stmt->bindParam(':status', $status, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll();
Плейсхолдери з назвами роблять код безпечнішим та зручнішим для читання, тоді як прив’язка параметрів за типом даних за допомогою bindParam() обмежує потенційні зловживання (а також помилки введення даних).
Використання MySQLi
MySQLi – це ще один безпечний метод, доступний для виконання запитів до бази даних у PHP. Він підтримує позиційні плейсхолдери та ідеально підходить, коли не використовується PDO.
У прикладі нижче наведено версію коду PDO для MySQLi:
// Hardcoded credentials for illustration only
$mysqli = new mysqli("localhost", "user", "pass", "testdb");
if ($mysqli->connect_error) {
die("Connection failed: " . $mysqli->connect_error);
}
$stmt = $mysqli->prepare("SELECT * FROM users WHERE name = ? AND status = ?");
$user = $_GET['user'];
$status = 1;
$stmt->bind_param("si", $user, $status); // "s" for string, "i" for integer
$stmt->execute();
$result = $stmt->get_result();
Тут функція bind_param() використовує плейсхолдери та забезпечує правильну обробку типів, захищаючи як числові, так і рядкові вхідні дані.
Додаткові методи запобігання SQLi в PHP застосунках
Параметризація не підлягає обговоренню, коли йдеться про запобігання SQL-ін’єкціям, але також дотримання додаткових практик зробить додаток надійнішим. Проте жодного з них окремо не достатньо для захисту від SQLi.
Перевірка та очищення вхідних даних користувача
Варто завжди застосовувати перевірку вхідних даних, щоб впевнитися, що вони відповідають очікуваним типам, форматам та значенням.
Кілька загальних правил, яких слід дотримуватися:
- Перевіряти очікувані типи значень. Якщо параметр може бути лише цілим числом, можна використовувати filter_var($input, FILTER_VALIDATE_INT).
- Для вибору із закритого та відомого набору застосовувати список дозволених значень (білий список), щоб приймати лише їх.
- Уникати покладання виключно на фільтрацію певних ключових слів (чорний список) або видалення спеціальних символів SQL.
Уникання динамічного SQL
Як правило, слід уникати динамічного SQL (побудова рядків запитів на основі вхідних даних користувача).
Якщо це абсолютно необхідно (наприклад, коли присутні динамічні назви стовпців), варто суворо перевіряти вхідні дані або використовувати зіставлення з попередньо визначеними значеннями, наприклад:
$columns = ['name', 'email', 'created_at'];
$sortColumn = in_array($_GET['sort'], $columns) ? $_GET['sort'] : 'name';
$query = "SELECT * FROM users ORDER BY $sortColumn";
Використання збережених процедур з параметрами
Збережені процедури – це попередньо скомпільовані SQL-процедури, що зберігаються на сервері бази даних. Вони можуть інкапсулювати складну логіку та, при використанні з параметризованими вхідними даними, надавати безпечний спосіб взаємодії з базою даних.
Ось приклад, який показує, як визначити просту збережену процедуру в MySQL, яка отримує записи користувачів на основі юзернейму. Цей код зазвичай виконується безпосередньо в клієнті командного рядка MySQL або в інструменті керування базами даних, як phpMyAdmin або MySQL Workbench:
CREATE PROCEDURE GetUser(IN uname VARCHAR(50))
BEGIN
SELECT * FROM users WHERE name = uname;
END
Треба викликати це з PHP за допомогою:
$stmt = $pdo->prepare("CALL GetUser(:uname)");
$stmt->bindParam(':uname', $_GET['user']);
$stmt->execute();
Хоча збережені процедури додають додатковий рівень захисту, їх все ще можна використовувати небезпечно. Варто бути обережними, щоб не створювати динамічний SQL всередині збережених процедур без безпечної обробки параметрів.
Обережна обробка помилок
Ніколи не варто розкривати необроблені помилки бази даних у продакшні. Слід вимкнути детальні повідомлення про помилки та безпечно їх логувати. Це запобігає отриманню зловмисниками інформації про структуру запитів або поведінку бази даних:
ini_set('display_errors', 0);
error_reporting(0);
// Log errors to a secure file outside the web root
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php_errors.log');
error_log("Application error at " . $_SERVER['REQUEST_URI']);
Принцип найменших привілеїв
Користувач бази даних, якого використовує PHP застосунок, повинен мати лише мінімальні необхідні дозволи. Хоча під час розробки часто зручно отримувати доступ до бази даних з повними правами адміністратора та робити все, що потрібно, не турбуючись про дозволи користувачів, запуск такого додатка в продакшні може бути надзвичайно небезпечним.
Щоб обмежити збитки у випадку, якщо зловмиснику вдасться експлуатувати ін’єкцію, не слід дозволяти жодних операцій, які не потрібні застосунку, особливо DROP, GRANT або доступ до адміністративних таблиць.
Тестування застосунків на вразливості SQL-ін’єкцій
Навіть якщо код відповідає всім найкращим практикам, все одно важливо перевірити, чи безпечний додаток на практиці, а не лише в теорії. Тестування на вразливості SQL-ін’єкцій має бути рутинною та автоматизованою частиною процесів розробки та безпеки.
Чому DAST важливий?
Інструменти динамічного тестування безпеки додатків (DAST) аналізують застосунок ззовні так, як це зробив би зловмисник. Вони імітують шкідливі вхідні дані в середовищах виконання та намагаються експлуатувати такі вразливості, як SQL-ін’єкція, без доступу до вихідного коду.
Якісне рішення DAST може:
- Автоматично виявляти точки SQL-ін’єкцій, що піддаються експлуатації, включаючи сліпі SQLi та ті, що залежать від часу. Важливо звертати увагу, чи охоплює інструмент всі типи SQL-ін’єкцій, як Invicti (раніше Netsparker), адже пропуск такої вразливості може дорого коштувати компаніям. Якщо ви хочете безкоштовно протестувати це рішення, то зверніться до нас зручним для вас способом.
- Надавати точні результати, безпечно використовуючи та підтверджуючи реальні вразливості, як це робить Invicti, замість того, щоб завантажувати розробників хибнопозитивними результатами.
- Інтегруватися з CI/CD для автоматичного сканування збірок, а також з тікетними системами для зручності управління результатами.
- Пропонувати практичні рекомендації щодо виправлення та деталі про вразливості, щоб допомогти командам швидше розв’язувати проблеми.
Таким чином ці найкращі практики дозволяють забезпечити захист PHP застосунків від SQLi, ефективно зменшуючи ризики безпеки.







