$pdo->query() та звичайної конкатенації рядків.prepare() -> execute().На попередніх двох лекціях нашого міні-практикуму ми раділи успішному додаванню та видаленню завдань. Ми писали код на зразок: query("INSERT INTO tasks ... '$title'") чи query("DELETE ... id = $id"). Але ми робили величезнув застереження: В ПРОФЕСІЙНОМУ ВЕБІ ТАК РОБИТИ ЗАБОРОНЕНО.
Зшивання (конкатенація) змінних від користувача безпосередньо в текст SQL-запиту — це відкриті двері, ключ від яких є в руках будь-якого школяра-хакера. Ми обговоримо концепцію SQL-ін’єкції та навчимось єдиному правильному способу захисту баз даних (PDO Prepared Statements).
Як виглядає вразливий код авторизації, написаний програмістом-новачком?
Він бере login та password із $_POST і відправляє їх у базу таким чином:
$user = $_POST['username']; // Гість щось ввів
$pass = $_POST['password']; // Гість щось ввів
// Небезпечне склеювання $user та $pass прямісінько в СТРУКТУРУ запиту
$sql = "SELECT id FROM users WHERE username = '$user' AND password = '$pass'";
$pdo->query($sql);
Хакеру достатньо зрозуміти, що ви використовуєте конкатенацію (зшивання тексту з одинарними лапками '$user').
Замість свого логіна він введе в HTML-форму дуже специфічний рядок:
admin' OR '1'='1
Давайте підставимо цей текст на місце нашої вразливої змінної $user і поглянемо, який SQL-запит відправиться базі даних у підсумку:
SELECT id FROM users WHERE username = 'admin' OR '1'='1' AND password = ''
MySQL читає це так:
“Дай мені айді юзера, якщо його логін ‘admin’, АБО ЯКЩО ОДИН ДОРІВНЮЄ ОДНОМУ.”
Оскільки 1 завжди дорівнює 1 (це беззаперечна істина TRUE), база даних проігнорує умови правильного пароля та пустить хакера під виглядом Адміністратора без пароля!
В іншій ситуації, при видаленні (“$id”), замір номеру завдання він може надіслати 1; DROP TABLE users;--. Скрипт виконає видалення першого завдання (1;), а другою командою під “крапкою з комою” видалить серверну таблицю з клієнтами. Ваш бізнес знищено.
Щоб 100% убезпечитись від SQLi, архітектори PDO створили концепт “Підготовлених виразів”.
Їхня мета: Жорстко розділити логіку SQL-команди від сирих Даних користувача. Дані ніколи не склеюються до SQL-запиту!
Розробник замість змінних вставляє в шаблон спеціальні плейсхолдери (замінники). У PDO Плейсхолдери бувають позиційними (знак питання ?) або “Іменованими” (з двокрапкою, наприклад :title).
/* Ми відправляємо Базі Даних ТІЛЬКИ СТРУКТУРУ без даних: */
INSERT INTO tasks (title) VALUES (:my_title)
prepare() -> execute()Тепер ми переписуємо всі наші старі вразливі CRUD-операції з використанням 2 етапів: “Підготовка структури” (prepare) і “Швидке вливання даних” (execute).
prepare (Шаблон)Ми надсилаємо на сервер бази даних виключно скелет запиту з іменованими плейсхолдерами.
$stmt = $pdo->prepare('DELETE FROM tasks WHERE id = :delete_id');
Що відбувається в базі? Вона “заморожує” цей синтаксис. Вона розуміє, що це запит на Видалення. Якщо туди пізніше прилетить DROP TABLE, вона розцінить це як ЗВИЧАЙНИЙ текст (ID завдання з іменем ‘DROP TABLE’), і синтаксис SQL не виконається.
execute (Дані)Тепер ми беремо отриманий $stmt і передаємо йому в круглих дужках [ ... ] реальний асоціативний масив даних з $_GET або $_POST.
<?php
require_once 'db.php';
// БЕЗПЕЧНЕ ВИДАЛЕННЯ (Без SQL-ін'єкцій)
if (isset($_GET['id'])) {
// 1. СТВОРЮЄМО СКЕЛЕТ
$stmt = $pdo->prepare("DELETE FROM tasks WHERE id = :d_id");
// 2. ЗШИВАЄМО
// Ключіマсиву - імена наших плейсхолдерів (бажано з двокрапкою).
// Значення - небезпечні змінні від HTTP
$stmt->execute([
':d_id' => $_GET['id']
]);
header('Location: index.php');
}
?>
INSERT (Створення завдання)Ми назавжди забуваємо конкатенацію і пишемо:
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 1. Підготовлена структура (Скільки змінних - стільки й плейсхолдерів)
$stmt = $pdo->prepare("INSERT INTO tasks (title, status) VALUES (:t_val, :s_val)");
// 2. Прив'язка і виконання команди (Замінюємо пустушки реальними POST)
$stmt->execute([
':t_val' => $_POST['title'], // 'admin' OR 1=1 тут не спрацює!
':s_val' => 0
]);
// Опціонально: функція lastInsertId() повідомить нам ID щойно створеного ресурсу!
// echo "Завдання успішно створено з номером " . $pdo->lastInsertId();
header("Location: index.php");
exit;
}
?>
$_POST і $_GET безпосередньо в текст SQL-запитів ($pdo->query).OR 1=1), ранньому закритті лапок або додаванні до запиту іншого запиту (через ;) для компрометації (злому або видалення) чужої інформації.:placeholders (через об’єктний метод $pdo->prepare()) та її активацію (через метод $stmt->execute([...])), який приймає конфігураційний масив цих плейсхолдерів та реальних значень.$pdo->lastInsertId() можна дізнатися унікальний згенерований Auto_Increment номер запису відразу ж після його вдалих спроб запису в базу через INSERT.true), не знаючи пароля?$pdo->prepare()?DROP TABLE users і передати її на ваш бекенд, що збудований виключно на Prepared Statements (через асоціативний масив execute([$data])), не призведе до знищення бази і падіння вашої компанії? Яким чином СУБД (База Даних) дивиться на цей текст?CRUD із 12 лекції. До якої з цих літер (і який саме класичний SQL запит їй відповідає) можна і доцільно застосувати метод “ідентифікації щойно доданого номера” $pdo->lastInsertId()?$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :my_mail");. Напишіть наступний рядок коду: масив і функцію активації, яка заповнить цей маркер :my_mail значенням “test@test.com” і виконає пошук.