27/08/2026
Tôi từng viết một hàm DELETE mà bản thân thấy khá “tiện” 😅
Bối cảnh: mỗi bảng có một chính sách lưu trữ dữ liệu khác nhau. Log giữ 30 ngày, đơn hàng giữ vài năm, dữ liệu tạm thì dọn hàng tuần. Viết riêng cho từng bảng thì mệt, nên tôi làm một hàm dùng chung: truyền vào tên bảng, truyền luôn cả cụm điều kiện dưới dạng chuỗi, đọc từ bảng config.
"DELETE FROM WHERE "
Gọn. Thêm bảng mới chỉ cần thêm một dòng cấu hình, không cần deploy. Lúc đó tôi còn thấy mình khá “thông minh”. 🤦♂️
📩 Cho đến một sáng, mail từ khách hàng nhảy vào inbox với tiêu đề: “ALERT: SQL INJECTION”.
Nội dung đại ý: Đội bảo mật bên họ rà soát và phát hiện một đoạn query cho phép can thiệp vào điều kiện xoá dữ liệu. Tôi mở lại đúng cái hàm “tiện” của mình và đặt câu hỏi lẽ ra phải hỏi từ đầu:
👉 “Cụm điều kiện đó ai cũng có thể điền ngẫu nhiên vào bảng config?”
Database chẳng có lỗi gì. Nó chỉ nhận lệnh và thực thi đúng như được yêu cầu. Vấn đề là ai đang “yêu cầu”, và yêu cầu đó có được kiểm soát hay không.
Đó cũng chính là bản chất của SQL Injection: hệ thống không phân biệt được đâu là dữ liệu, đâu là câu lệnh. Dính ở SELECT thì lộ dữ liệu. Dính ở DELETE thì mất dữ liệu, theo nghĩa đen.
🔍 NHẬN DIỆN: Những pattern nên khiến bạn dừng lại khi review code
🚩 Câu SQL ghép bằng dấu cộng hoặc string interpolation, có biến nằm giữa
🚩 Tham số hàm tên kiểu where, condition, filter, orderBy, tableName, rawSql. Nhận string thì gần như chắc chắn đang nhận cả câu lệnh
🚩 Điều kiện query đọc ra từ config, từ file, từ một bảng khác — dữ liệu lưu trong hệ thống không đồng nghĩa với dữ liệu đáng tin
🚩 Bạn phải tự đặt dấu nháy đơn quanh biến. Việc đó là của driver, không phải của bạn
🚩 Có đoạn escape thủ công, replace dấu nháy. Đó là chống cháy, không phải chống lỗI
💡 Mẹo nhanh: Nếu nhìn một câu SQL mà phải đoán xem lúc chạy nó sẽ biến thành gì, chỗ đó nên kiểm tra lại.
🛠️ CÁCH XỬ LÝ:
✅ Prepared statement cho phần giá trị. Tham số hoá giá trị, đừng tham số hoá cả câu lệnh.
✅ Phần buộc phải động (tên bảng, tên cột) thì whitelist. Không nằm trong danh sách thì không chạy.
✅ Với retention, đừng cho cấu hình cả cụm điều kiện. Cho cấu hình đúng thứ cần thay đổi: cột thời gian và số ngày giữ lại. Câu lệnh do code dựng, không do config dựng.
✅ Job chạy ngầm cũng cần phân quyền tối thiểu và log lại số dòng bị ảnh hưởng.
✅ Dry-run đếm trước khi xoá thật, cộng thêm backup.
May là lần đó chưa có thiệt hại thực tế. Nhưng bài học thì ở lại:
Lỗ hổng đôi khi không đến từ những pha tấn công tinh vi. Nó có thể bắt đầu từ một thứ được viết chỉ vì… “tiện”. 😅
💬 Bạn đã bao giờ viết một cái gì đó mà nó... "tiện" kiểu vậy chưa?