SQL Injection là lỗ hổng phổ biến và nguy hiểm nhất của ứng dụng web: kẻ tấn công chèn mã SQL vào ô nhập để đọc, sửa, xóa dữ liệu. Bài này chỉ ra cách tấn công hoạt động và cách phòng chống bằng prepared statement cùng các lớp phòng thủ bổ sung.
- Hiểu cơ chế SQL Injection qua ví dụ
- Nhận diện mã dễ bị tấn công
- Phòng chống bằng prepared statement (MySQLi, PDO)
- Phòng thủ nhiều lớp: whitelist, quyền tối thiểu, ẩn lỗi
- Injection
- Chèn mã vào nơi lẽ ra chỉ có dữ liệu.
- Prepared statement
- Tách mẫu SQL và dữ liệu, gửi riêng tới máy chủ.
- Placeholder
- Dấu ? hoặc :ten giữ chỗ cho dữ liệu.
- Whitelist
- Danh sách giá trị được phép.
Khái niệm và lý thuyết
Nguyên nhân gốc: ghép chuỗi dữ liệu người dùng vào câu SQL, máy chủ không phân biệt được đâu là lệnh, đâu là dữ liệu. Cách chữa tận gốc: prepared statement: máy chủ biên dịch mẫu SQL trước, dữ liệu gửi sau và luôn được coi là giá trị thuần.
Tấn công hoạt động thế nào?
<?php
// MÃ DỄ BỊ TẤN CÔNG
$sql = "SELECT * FROM nguoidung WHERE email = '$email' AND mat_khau = '$mk'";Nhập email là ' OR '1'='1' -- , câu lệnh thành:
SELECT * FROM nguoidung WHERE email = '' OR '1'='1' -- ' AND mat_khau = 'x'
-- điều kiện luôn đúng, phần sau bị chú thích -> đăng nhập không cần mật khẩu
-- Ô tìm kiếm: ?id=1 UNION SELECT email, mat_khau_hash FROM nguoidung
-- -> đọc trộm bảng khácPhòng chống: prepared statement
<?php
// MySQLi
$st = $conn->prepare("SELECT id, mat_khau_hash FROM nguoidung WHERE email = ?");
$st->bind_param("s", $email);
$st->execute();
$nd = $st->get_result()->fetch_assoc();
if ($nd && password_verify($mk, $nd["mat_khau_hash"])) { /* đăng nhập */ }
// PDO
$pdo = new PDO("mysql:host=localhost;dbname=qlsv;charset=utf8mb4", "qlsv_app", $pass,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => false]);
$st = $pdo->prepare("SELECT * FROM sinhvien WHERE ho_ten LIKE :q AND lop_id = :lop");
$st->execute([":q" => "%$q%", ":lop" => $lop_id]);
$ds = $st->fetchAll(PDO::FETCH_ASSOC);Không đặt dấu ? trong nháy. Với LIKE, đặt % trong giá trị, không trong mẫu SQL.
Khi không dùng được placeholder
<?php
// Tên cột, hướng sắp xếp: dùng whitelist
$cot = in_array($_GET["sort"] ?? "", ["ho_ten", "ngay_sinh", "ma_sv"], true) ? $_GET["sort"] : "ho_ten";
$huong = ($_GET["dir"] ?? "") === "desc" ? "DESC" : "ASC";
$st = $conn->prepare("SELECT * FROM sinhvien ORDER BY $cot $huong LIMIT ? OFFSET ?");
$st->bind_param("ii", $limit, $offset);
// IN (...) động
$dau = implode(",", array_fill(0, count($ids), "?"));
$st = $conn->prepare("SELECT * FROM sinhvien WHERE id IN ($dau)");
$st->bind_param(str_repeat("i", count($ids)), ...$ids);Phòng thủ nhiều lớp
- Prepared statement cho mọi truy vấn có dữ liệu bên ngoài.
- Validate kiểu, độ dài, định dạng trước khi truy vấn.
- Tài khoản CSDL quyền tối thiểu (bài 9).
- Không hiển thị lỗi SQL cho người dùng; ghi log.
- Dùng ORM/Query Builder (Eloquent, PDO) đã bind sẵn.
| Cách cũ | Vấn đề |
|---|---|
| mysqli_real_escape_string | Dễ sai, chỉ là lớp phụ |
| Lọc từ khóa SELECT, DROP | Dễ lách, hỏng dữ liệu hợp lệ |
| Giấu lỗi, đổi tên bảng | Không ngăn được tấn công |
Luyện tập an toàn trên DVWA hoặc lab miễn phí tại portswigger.net/web-security: tự khai thác rồi tự vá.
Chỉ thử tấn công trên hệ thống của chính bạn hoặc được phép. Tấn công hệ thống người khác là vi phạm pháp luật.
- Viết trang đăng nhập dễ bị tấn công trên localhost, thử tấn công, rồi sửa bằng prepared statement.
- Viết tìm kiếm có sắp xếp theo cột người dùng chọn, dùng whitelist.
- Viết hàm lấy sinh viên theo danh sách id với IN (...) động.
- Rà soát dự án của bạn: 100% truy vấn dùng prepared statement.
- Ghép chuỗi SQL = lỗ hổng; prepared statement chữa tận gốc.
- Placeholder cho giá trị; whitelist cho tên cột, hướng sắp xếp.
- Validate, quyền tối thiểu, ẩn lỗi là lớp bổ sung.