Впровадження SQL-коду (SQL ін'єкція) - один із поширених способів злому сайтів, що працюють із базами даних. Спосіб заснований на впровадженні в запит довільного SQL-коду. Впровадження SQL дає змогу хакеру виконати довільний запит до бази даних (прочитати вміст будь-яких таблиць, видалити, змінити або додати дані).
Атака цього типу можлива, коли недостатньо фільтруються вхідні дані під час використання в SQL-запитах.
Припустимо, на нашому сайті є сторінка показу історії погодних спостережень для одного міста. Ідентифікатор цього міста передається в посиланні в параметрі запиту: /weather.php?city_id=<ID>, де ID - це первинний ключ міста. У PHP-сценарії використовуємо цей параметр для підстановки в SQL запит:
$city_id = $_GET['city_id'];
$res = mysqli_query($link, "SELECT * FROM weather_log WHERE city_id = " . $city_id);
Якщо на сервері передано параметр city_id, що дорівнює 10 (/weather.php?city_id=10), то виконається SQL-запит:
SELECT * FROM weather_log WHERE city_id = 10
Але якщо зловмисник передасть як параметр id рядок -1 OR 1=1, то виконається запит:
SELECT * FROM weather_log WHERE city_id = -1 OR 1=1
Додавання у вхідні параметри конструкцій мови SQL (замість простих значень) змінює логіку виконання всього SQL запиту. У цьому прикладі замість показу даних по одному місту, будуть отримані дані по всіх містах, тому що вираз 1 = 1 завжди істинний. Замість виразу SELECT ... міг бути вираз на оновлення даних, і тоді наслідки були б ще серйознішими.
Відсутність належної обробки параметрів SQL-запиту - це одна з найсерйозніших вразливостей. Ніколи не вставляйте дані від користувача в SQL-запити "як є"!
У SQL-запити часто підставляються цілочисельні значення, отримані від користувача. У прикладах вище використовувався ідентифікатор міста, отриманий з параметрів запиту. Цей ідентифікатор можна примусово привести до числа. Так ми виключимо появу в ньому небезпечних виразів. Якщо хакер передасть у цьому параметрі замість числа SQL код, то результатом приведення буде нуль, і логіка всього SQL-запиту не зміниться.
PHP вміє присвоювати змінній новий тип. Цей код примусово призначить змінній цілочисельний тип:
$city_id = $_GET['city_id'];
settype($city_id, 'integer');
Після перетворення змінну $city_id можна без побоювання використовувати в SQL-запитах.
Що робити, якщо в SQL запит потрібно підставити строкове значення? Наприклад, на сайті є можливість пошуку міста за його назвою. Форма пошуку передасть пошуковий запит у GET-параметр, а ми використаємо його в SQL-запиті:
$city_name = $_GET['search'];
$sql = "SELECT * FROM cities WHERE name LIKE('%$city_name%')";
Але якщо в параметрі city_name буде символ лапки, то сенс запиту можна кардинально змінити. Передавши в search_text значення ')+and+(id<>'0, ми виконаємо запит, що виведе список усіх міст:
SELECT * FROM cities WHERE name LIKE('%') AND (id<>'0%'))
Сенс запиту змінився, тому що лапка з параметра запиту вважається керуючим символом: MySQL визначає закінчення значення за символом лапки після нього, тому самі значення лапки містити не повинні. Очевидно, приведення до числового типу не підходить для строкових значень. Тому, щоб убезпечити строкове значення, використовують операцію екранування.
Екранування додає в рядку перед лапками (та іншими спецсимволами) знак зворотного слеша \. Така обробка позбавляє лапки їхнього статусу - вони більше не визначають кінець значення і не можуть вплинути на логіку SQL-виразу.
За екранування значень відповідає функція mysqli_real_escape_string(). Цей код обробить значення з параметра, зробивши його безпечним для використання в запиті:
$city_name = mysqli_real_escape_string($link, $_GET['search']);
$sql = "SELECT * FROM cities WHERE name LIKE('%$city_name%')";
Вид атак типу "SQL-ін'єкція" можливий, тому що значення (дані) для SQL-запиту передаються разом із самим запитом. Оскільки дані не відокремлені від SQL-коду, вони можуть впливати на логіку всього виразу. На щастя, MySQL пропонує спосіб передачі даних окремо від коду. Такий спосіб називається підготовленими запитами.
Виконання підготовлених запитів складається з двох етапів: спочатку формується шаблон запиту - звичайний SQL-вираз, але без дійсних значень, а потім, окремо, в MySQL передаються значення для цього шаблону.
Перший етап називається підготовкою, а другий - виразом. Підготовлений запит можна виконувати кілька разів, передаючи туди різні значення.
Етап підготовки. На етапі підготовки формується SQL-запит, де на місці значень будуть знаходитися знаки запитання - плейсхолдери. Ці плейсхолдери надалі будуть замінені на реальні значення. Шаблон запиту надсилається на сервер MySQL для аналізу та синтаксичної перевірки. Приклад:
$sql = "SELECT * FROM cities WHERE name = ?";
$stmt = mysqli_prepare($link, $sql);
Цей код сформує підготовлений вираз для виконання вашого запиту.
За підготовкою йде виконання. Під час запуску запиту PHP прив'язує до плейсхолдерів реальні значення і посилає їх на сервер. За передачу значень у підготовлений запит відповідає функція mysqli_stmt_bind_param(). Вона приймає тип і самі змінні:
mysqli_stmt_bind_param($stmt, 's', $_GET['search']);
Після виконання запиту отримати його результат у форматі mysqli_result можна функцією mysqli_stmt_get_result():
// виконання запиту
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
// читання даних
while ($row = mysqli_fetch_assoc($res)) {
// асоціативний масив із черговим записом із результату
var_dump($row);
}
Значення прив'язаних до запиту змінних сервер екранує автоматично. Прив'язані змінні відправляються на сервер окремо від запиту і не можуть впливати на нього. Сервер використовує ці значення безпосередньо в момент виконання, вже після того, як було оброблено шаблон виразу. Прив'язані параметри не потребують екранування, оскільки вони ніколи не підставляються безпосередньо в рядок запиту.