nmk

Лекція 17 (2 години). Безпека веб-застосунків: Паролі та XSS

План лекції

  1. Чому зберігати паролі “як є” (Plain text) — це злочин.
  2. Концепція Хешування: алгоритм Bcrypt (password_hash).
  3. Реєстрація користувача: перевірка унікальності Email та збереження хешу.
  4. Авторизація (Логін): порівняння введеного пароля з хешем з БД (password_verify).

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

Вступ

Ми навчилися створювати користувачів та запускати сесії. Але є одна світова проблема безпеки, яка щороку призводить до витоку мільярдів акаунтів (згадайте злами LinkedIn чи Yahoo). Це збереження паролів “як є” у базі даних (у колонці password лежить 123456). Якщо розробник так робить, і базу даних сайту викраде хакер (або навіть сам адміністратор-розробник вирішить підглянути) — всі ці люди втратять доступ не лише до вашого сайту, а й до своїх пошт та інтернет-банкінгів, бо більшість використовує один пароль скрізь. У цій лекції ми навчимось грамотно захищати дані наших майбутніх користувачів.


1. Головне правило: Ніколи не зберігайте паролі відкритим текстом!

Код з Лекції 9, де ми перевіряли if ($_POST['password'] === '123'), або вставка сирого пароля в таблицю users — це категоричне табу для реальних проєктів (ми робили так лише в навчальних цілях для розуміння сесій).

База даних повинна бути спроєктована так, щоб НАВІТЬ ВИ (як її власник і адміністратор), глянувши в таблицю users через phpMyAdmin, не могли дізнатися справжній пароль ваших клієнтів.

2. Концепція Хешування: password_hash

Хешування — це одностороння математична м’ясорубка. Ви кидаєте туди слово qwerty, крутите ручку, і на виході отримуєте “фарш” (рядок із 60 випадкових символів: $2y$10$xyz123abc...).

Головне правило хешу: З цього “фаршу” вже ніколи і ні за яких умов неможливо математично зібрати назад початкове слово qwerty. Алгоритм працює тільки в один бік. Якщо хакер вкраде базу з хешами — вони для нього марні (якщо алгоритм надійний).

В PHP для цього є геніально проста і максимально міцна функція: password_hash(). Вона автоматично генерує додаткову “сіль” (домішку), тому навіть у двох користувачів з однаковим паролем qwerty будуть абсолютно різні кінцеві хеші!

<?php
// Пароль, який користувач ввів при РЕЄСТРАЦІЇ
$plainPassword = "MySuperSecretPassword_2024";

// Перетворюємо його у незворотний Хеш за допомогою алгоритму Bcrypt (кодується під капотом):
$hashToSave = password_hash($plainPassword, PASSWORD_DEFAULT);

// Виведе щось на зразок:
// $2y$10$M9bZ/08xHbZ7YcQ1N5W.pOC6x.HZmI5xH9X.qP.Z.8H3p.TzB/IJi
echo "Будемо зберігати в базі ЦЕ: " . $hashToSave;
?>

Зверніть увагу: Стовпець password в таблиці users (в phpMyAdmin) ОБОВ’ЯЗКОВО має мати тип VARCHAR(255), щоб цей довгий хеш туди точно помістився. Якщо ви поставите VARCHAR(20), він обріжеться посередині і користувач ніколи не зможе увійти в акаунт!

3. Реєстрація: Запис нового користувача у БД

Як виглядає грамотний файл реєстрації register.php? Він повинен прийняти email та password, перевірити, чи немає вже такого email в системі (щоб не було дублювання), ЗХЕШУВАТИ пароль і лише потім зробити SQL INSERT.

<?php
require_once 'db.php'; // Наш PDO

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $email = $_POST['user_email'];
    $rawPass = $_POST['user_password'];

    // 1. Спочатку перевіряємо, чи не зайнятий Email?
    $checkStmt = $pdo->prepare('SELECT id FROM users WHERE email = :mail');
    $checkStmt->execute([':mail' => $email]);

    // Якщо fetch() щось знайшов (НЕ false), значить такий рядок вже є!
    if ($checkStmt->fetch()) {
        die("Помилка: Ця електронна пошта вже зареєстрована!");
    }

    // 2. Хешуємо пароль (головний момент лекції)
    $hashedPassword = password_hash($rawPass, PASSWORD_DEFAULT);

    // 3. Зберігаємо користувача в БД (вставляємо ХЕШ, а не $rawPass)
    $insertStmt = $pdo->prepare('INSERT INTO users (email, password) VALUES (:e, :p)');
    $insertStmt->execute([
        ':e' => $email,
        ':p' => $hashedPassword // Відправляємо безпечний $2y$10...
    ]);

    echo "Реєстрація успішна! Тепер можете увійти.";
}
?>

4. Авторизація (Логін): Перевірка правильності (password_verify)

Отже, користувач зареєструвався. В базі лежить хеш. Наступного дня він приходить на сайт і вводить у форму знову qwerty. Але як нам перевірити, що пароль правильний, якщо “розшифрувати” хеш з бази технічно неможливо?

Тут застосовується магія: ми хешуємо те, що користувач ввів сьогодні, і порівнюємо отриманий результат з хешем, який лежить у базі з учорашнього дня! В PHP є спеціальна функція-напарник: password_verify(сирий_текст, хеш_з_бази). Вона внутрішньо дістає “сіль” з хешу, робить математику і каже: true (відповідає) або false (бреше).

<?php
// Файл логіну: login.php
require_once 'db.php';
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $emailAttempt = $_POST['email'];
    $passAttempt  = $_POST['password']; // Сирий 'qwerty'

    // 1. Шукаємо користувача за його Поштою
    $stmt = $pdo->prepare('SELECT * FROM users WHERE email = :e');
    $stmt->execute([':e' => $emailAttempt]);
    $user = $stmt->fetch(PDO::FETCH_ASSOC);

    // 2. Якщо користувача взагалі не існує в базі
    if (!$user) {
        die("Користувача з такою поштою не знайдено!");
    }

    // 3. Користувач є. Його хеш з бази лежить у $user['password'].
    // Порівнюємо сирий ввід з базисним хешем:
    if (password_verify($passAttempt, $user['password'])) {

        // УСПІХ! Паролі "зійшлися". Запускаємо сесію!
        $_SESSION['user_id'] = $user['id'];
        $_SESSION['logged_in'] = true;

        header('Location: profile.php');
        exit;

    } else {
        echo "Невірний пароль!";
    }
}
?>

Висновки

  1. Зберігання текстових, прозорих паролів користувачів у Базі Даних (Plain text) порушує всі світові стандарти і є ознакою низької кваліфікації. У разі хакерського витоку бази, дані ваших клієнтів (які використовують однакові паролі всюди) будуть повністю скомпрометовані.
  2. Натомість використовується одностороннє математичне перетворення (Хешування) алгоритмів типу Bcrypt.
  3. Для генерації захищеного зліпка при реєстрації використовують функцію password_hash(). Вона автоматично застосовує “Соління” пароля і генерує довжелезний специфічний хеш-рядок.
  4. Оскільки розшифрувати пароль із Хеша неможливо, під час авторизації (Логіну) виконується операція Порівняння Хешів. Сирий пароль, введений у форму зіставляється з тим “Джерельним рядком”, що був створений і лежить в БД, за допомогою спеціальної функції PHP password_verify(). Вона поверне true, якщо все збігається.
  5. Для уникнення технічних збоїв і “обрізання” хешу під час INSERT, таблиця облікових записів users, при її проєктуванні, повинна мати стовпець password з довжиною мінімум VARCHAR(255).

Джерела

  1. Офіційна документація password_hash: https://www.php.net/manual/ru/function.password-hash.php
  2. Документація password_verify: https://www.php.net/manual/ru/function.password-verify.php

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

  1. Згадайте процес хешування. Чому спроба хакера “розшифрувати” вкрадений з вашої бази даних хеш ($2y...) є математично неможливою та вкрай ресурсоємною? Чим він принципово відрізняється від процесу “шифрування” (наприклад перекладу в Base64)?
  2. Яким мінімальним типом (і з якою розрядністю) ви зобов’язані спроєктувати колонку "password" під час створення таблиці users у phpMyAdmin? Що фізично станеться з вашим користувачем, якого ви додасте в БД, якщо ви задасте цій колонці специфікацію VARCHAR(16)?
  3. Що робить допоміжна функція password_verify і з яких двох “аргументів” вона складається під час використання? (Де вона бере сирий порівняльний пароль, а звідки вона дістає еталонний хеш?).
  4. Назвіть (або опишіть загальними словами) ключовий логічний паттерн та етап реєстрації нового Користувача ($registerForm). Яку додаткову “Перевірку Запитом (SELECT…)” ви маєте обов’язково зробити до того, як застосувати INSERT INTO, якщо ми не бажаємо мати два профілі з повністю однаковим Email у базі?
  5. Чому використання застарілих алгоритмів хешування у PHP (наприклад легендарний md5($pswd)) сьогодні категорично не рекомендується в комерційних проєктах?