nmk

Лекція 14 (2 години). Безпека БД та підготовлені запити

План лекції

  1. Вади методу $pdo->query() та звичайної конкатенації рядків.
  2. Що таке SQL-ін’єкція (SQL Injection) і як її виконують.
  3. Безпека через Prepared Statements (Підготовлені вирази).
  4. Переписування CRUD-операцій на prepare() -> execute().

Перелік умовних скорочень

Вступ

На попередніх двох лекціях нашого міні-практикуму ми раділи успішному додаванню та видаленню завдань. Ми писали код на зразок: query("INSERT INTO tasks ... '$title'") чи query("DELETE ... id = $id"). Але ми робили величезнув застереження: В ПРОФЕСІЙНОМУ ВЕБІ ТАК РОБИТИ ЗАБОРОНЕНО. Зшивання (конкатенація) змінних від користувача безпосередньо в текст SQL-запиту — це відкриті двері, ключ від яких є в руках будь-якого школяра-хакера. Ми обговоримо концепцію SQL-ін’єкції та навчимось єдиному правильному способу захисту баз даних (PDO Prepared Statements).


1. Конкатенація рядків та її катастрофізм

Як виглядає вразливий код авторизації, написаний програмістом-новачком? Він бере 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);

2. Що таке SQL-ін’єкція (SQLi)

Хакеру достатньо зрозуміти, що ви використовуєте конкатенацію (зшивання тексту з одинарними лапками '$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;), а другою командою під “крапкою з комою” видалить серверну таблицю з клієнтами. Ваш бізнес знищено.

3. Підготовлені вирази (Prepared Statements) та Плейсхолдери

Щоб 100% убезпечитись від SQLi, архітектори PDO створили концепт “Підготовлених виразів”.

Їхня мета: Жорстко розділити логіку SQL-команди від сирих Даних користувача. Дані ніколи не склеюються до SQL-запиту!

Розробник замість змінних вставляє в шаблон спеціальні плейсхолдери (замінники). У PDO Плейсхолдери бувають позиційними (знак питання ?) або “Іменованими” (з двокрапкою, наприклад :title).

/* Ми відправляємо Базі Даних ТІЛЬКИ СТРУКТУРУ без даних: */
INSERT INTO tasks (title) VALUES (:my_title)

4. Життєвий цикл PDO: prepare() -> execute()

Тепер ми переписуємо всі наші старі вразливі CRUD-операції з використанням 2 етапів: “Підготовка структури” (prepare) і “Швидке вливання даних” (execute).

1-й Етап: prepare (Шаблон)

Ми надсилаємо на сервер бази даних виключно скелет запиту з іменованими плейсхолдерами. $stmt = $pdo->prepare('DELETE FROM tasks WHERE id = :delete_id'); Що відбувається в базі? Вона “заморожує” цей синтаксис. Вона розуміє, що це запит на Видалення. Якщо туди пізніше прилетить DROP TABLE, вона розцінить це як ЗВИЧАЙНИЙ текст (ID завдання з іменем ‘DROP TABLE’), і синтаксис SQL не виконається.

2-й Етап: 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;
}
?>

Висновки

  1. Загальновизнаним антипатерном є “Зшивання”, “Конкатенація” і пряма підстановка даних із середовища $_POST і $_GET безпосередньо в текст SQL-запитів ($pdo->query).
  2. Вразливість типу SQL Injection полягає у навмисній “підміні” операторів пошуку (напр., OR 1=1), ранньому закритті лапок або додаванні до запиту іншого запиту (через ;) для компрометації (злому або видалення) чужої інформації.
  3. Абсолютним рішенням проблеми безпеки є “Prepared Statements” (Підготовлені Вирази), що повністю розмежовують логіку SQL і сирі дані клієнта. Будь-які “шкідливі” домішки база даних розцінює виключно як звичайний рядок тексту.
  4. Технологія підготовлених запитів вимагає розділення коду на складання структури із маркерами :placeholders (через об’єктний метод $pdo->prepare()) та її активацію (через метод $stmt->execute([...])), який приймає конфігураційний масив цих плейсхолдерів та реальних значень.
  5. За допомогою методу об’єкта підключення $pdo->lastInsertId() можна дізнатися унікальний згенерований Auto_Increment номер запису відразу ж після його вдалих спроб запису в базу через INSERT.

Джерела

  1. Prepared Statements (Manual): https://www.php.net/manual/ru/pdo.prepared-statements.php
  2. Що таке SQL Injection: https://owasp.org/www-community/attacks/SQL_Injection

Запитання для самоперевірки

  1. Поясніть логіку хакера (на прикладі логіну через “OR 1=1”), який використовує хибу програміста в прямому зшиванні змінних. Як він примудряється “виконати” запит на автентифікацію з правильним результатом (true), не знаючи пароля?
  2. Яким візуальним математичним символом (або знаком пунктуації) найчастіше розділяються системні Плейсхолдери (Іменовані) в тілі SQL-запиту, підготованого для методу $pdo->prepare()?
  3. Чому спроба хакера написати команду DROP TABLE users і передати її на ваш бекенд, що збудований виключно на Prepared Statements (через асоціативний масив execute([$data])), не призведе до знищення бази і падіння вашої компанії? Яким чином СУБД (База Даних) дивиться на цей текст?
  4. Згадайте архітектурну модель CRUD із 12 лекції. До якої з цих літер (і який саме класичний SQL запит їй відповідає) можна і доцільно застосувати метод “ідентифікації щойно доданого номера” $pdo->lastInsertId()?
  5. Ви написали код $stmt = $pdo->prepare("SELECT * FROM users WHERE email = :my_mail");. Напишіть наступний рядок коду: масив і функцію активації, яка заповнить цей маркер :my_mail значенням “test@test.com” і виконає пошук.