PHP / SQL Injection хамгаалалт

SQL Injection хамгаалалт

SQL Injection бол вэбийн хамгийн аюултай халдлагуудын нэг — OWASP Top 10 жагсаалтад жил бүр орддог. Халдагч SQL query-д хортой код оруулж, бүх өгөгдлийн санг унших, устгах, өөрчлөх боломж авдаг. Гэхдээ PDO prepared statement ашигласнаар энэ халдлагыг бүрэн зогсооно.

SQL Injection хэрхэн ажилладаг вэ?

Хамгаалалтгүй query иймэрхүү харагдана:

php
<?php
// ❌ ҮХЛИЙН АЮУЛТАЙ — хэзээ ч ингэж бичиж болохгүй!
$имэйл = $_POST['имэйл'];
$нууц_үг = $_POST['нууц_үг'];

$query = "SELECT * FROM users WHERE email = '$имэйл' AND password = '$нууц_үг'";
// Хэрэглэгч нууц_үг талбарт дараах зүйлийг бичвэл:
// ' OR '1'='1
//
// Query нь болно:
// SELECT * FROM users WHERE email = 'test@mail.mn' AND password = '' OR '1'='1'
// '1'='1' үргэлж true → нэвтрэлт үргэлж амжилттай!

// Бүр ч хортой:
// нууц_үг = '; DROP TABLE users; --
// Бүх хэрэглэгчийн мэдээлэл устана!
?>

Нэг л мөр буруу бичихэд бүх өгөгдлийн сан эрсдэлд орно.

Prepared statement — цорын ганц зөв арга

PDO prepared statement нь SQL бүтэц ба өгөгдлийг тусад нь явуулдаг тул халдлага боломжгүй болно:

php
<?php
$pdo = new PDO('mysql:host=localhost;dbname=myapp;charset=utf8mb4', 'root', '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// ✓ Prepared statement — placeholder : ашиглана
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :имэйл AND active = 1");
$stmt->execute([':имэйл' => $_POST['имэйл']]);
$хэрэглэгч = $stmt->fetch(PDO::FETCH_ASSOC);

// ? placeholder ашиглах хэлбэр
$stmt = $pdo->prepare("SELECT * FROM products WHERE category = ? AND price <= ?");
$stmt->execute([$_GET['төрөл'], $_GET['үнэ']]);
$бүтээгдэхүүнүүд = $stmt->fetchAll(PDO::FETCH_ASSOC);

// Яагаад аюулгүй вэ?
// PDO өгөгдлийг SQL-ийн нэг хэсэг биш харин шууд утга болгон явуулдаг.
// '; DROP TABLE users; -- бол зүгээр л хайлтын мөр болно.
?>

CRUD үйлдлүүд — prepared statement-тай

php
<?php
class UserRepository
{
    private PDO $pdo;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    // Нэвтрэлт шалгах
    public function нэвтрэлт(string $имэйл, string $нууц_үг): ?array
    {
        $stmt = $this->pdo->prepare(
            "SELECT id, name, password_hash FROM users WHERE email = :имэйл LIMIT 1"
        );
        $stmt->execute([':имэйл' => $имэйл]);
        $хэрэглэгч = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($хэрэглэгч && password_verify($нууц_үг, $хэрэглэгч['password_hash'])) {
            return $хэрэглэгч;
        }
        return null;
    }

    // Шинэ хэрэглэгч нэмэх
    public function үүсгэх(string $нэр, string $имэйл, string $нууц_үг): int
    {
        $stmt = $this->pdo->prepare(
            "INSERT INTO users (name, email, password_hash, created_at)
             VALUES (:нэр, :имэйл, :нууц_үг, NOW())"
        );
        $stmt->execute([
            ':нэр'    => $нэр,
            ':имэйл'  => $имэйл,
            ':нууц_үг' => password_hash($нууц_үг, PASSWORD_BCRYPT),
        ]);
        return (int) $this->pdo->lastInsertId();
    }

    // Устгах
    public function устгах(int $id): bool
    {
        $stmt = $this->pdo->prepare("DELETE FROM users WHERE id = :id");
        $stmt->execute([':id' => $id]);
        return $stmt->rowCount() > 0;
    }
}
?>

Нэмэлт хамгаалалтын давхаргууд

Prepared statement нь гол хамгаалалт боловч нэмэлт дүрмүүд мөн чухал:

php
<?php
// 1. Оролтыг төрлөөр шалгах
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false || $id === null || $id <= 0) {
    http_response_code(400);
    die("Хүчингүй ID.");
}

// 2. LIKE хайлтад wildcard тэмдэгтийг оgolох
$хайлт = $_GET['q'] ?? '';
$аюулгүй = '%' . str_replace(['%', '_', '\\'], ['\\%', '\\_', '\\\\'], $хайлт) . '%';
$stmt = $pdo->prepare("SELECT * FROM products WHERE name LIKE :хайлт");
$stmt->execute([':хайлт' => $аюулгүй]);

// 3. Динамик ORDER BY — whitelist ашиглана (placeholder ажиллахгүй!)
$зөвшөөрөгдсөн = ['name', 'price', 'created_at'];
$баганы_нэр = $_GET['sort'] ?? 'name';
if (!in_array($баганы_нэр, $зөвшөөрөгдсөн, true)) {
    $баганы_нэр = 'name';
}
// Зөвшөөрөгдсөн утгыг шууд оруулна — аюулгүй
$stmt = $pdo->query("SELECT * FROM products ORDER BY $баганы_нэр ASC");

// 4. Өгөгдлийн сангийн хэрэглэгчийн эрхийг хязгаарлах
// Вэб апп-д зөвхөн SELECT, INSERT, UPDATE, DELETE эрх өгнө
// DROP, CREATE, ALTER эрх огт шаардлагагүй
?>

Нууц үгийг зөв хадгалах

SQL Injection-тай холбоотой нийтлэг алдаа:

php
<?php
// ❌ Хэзээ ч нууц үгийг ийм хадгалж болохгүй!
// MD5, SHA1 → хурдан crack хийж болно
// Plaintext → өгөгдлийн сан алдагдахад бүгд ил болно

// ✓ password_hash() — цорын ганц зөв арга
$нууц_үг_хэш = password_hash($нууц_үг, PASSWORD_BCRYPT, ['cost' => 12]);
// $2y$12$... хэлбэрийн hash үүснэ — database-д хадгална

// ✓ Шалгахдаа
if (password_verify($оролт, $хадгалагдсан_хэш)) {
    echo "Зөв нууц үг!";
}

// Нууц үгийн шаардлага
function нууц_үг_шалгах(string $нууц_үг): array
{
    $алдаанууд = [];
    if (strlen($нууц_үг) < 8)                     $алдаанууд[] = "Хамгийн багадаа 8 тэмдэгт";
    if (!preg_match('/[A-Z]/', $нууц_үг))           $алдаанууд[] = "Нэг том үсэг шаардлагатай";
    if (!preg_match('/[0-9]/', $нууц_үг))           $алдаанууд[] = "Нэг тоо шаардлагатай";
    return $алдаанууд;
}
?>

Дараагийн хичээлд:

CSRF (Cross-Site Request Forgery) халдлагаас хамгаалах аргыг судална. Token-д суурилсан хамгаалалт хэрхэн ажилладаг, PHP-д хэрхэн хэрэгжүүлэхийг үзнэ.