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-д хэрхэн хэрэгжүүлэхийг үзнэ.