Đừng Bao Giờ Tin Người Dùng: Nghệ Thuật Validate, Sanitize và Giới Hạn Scope Filter
Câu chuyện kinh điển của ngành IT: Một ngày đẹp trời, hệ thống báo cáo doanh thu bỗng dưng... trắng tinh. Check log truy cập, mình phát hiện ra một request lạ lùng ở API tìm kiếm sản phẩm:
GET /api/products?search=' OR 1=1; DROP TABLE products; --
Vâng, một pha SQL Injection thô thiển nhưng lại hiệu quả. Hoá ra, cậu em trong team đã dùng phép cộng chuỗi huyền thoại để build câu SQL thay vì dùng Parameterized Query.
Là một Vibe Coder, chúng ta hướng tới sự nhàn nhã. Mà muốn nhàn, hệ thống phải "mình đồng da sắt" từ vòng gửi xe. Quy tắc số 1 và duy nhất của Backend: Tuyệt đối KHÔNG BAO GIỜ tin tưởng dữ liệu từ người dùng gửi lên. Hôm nay, anh em mình sẽ cùng mổ xẻ 3 tấm khiên vững chắc nhất: Validate, Sanitize và Limit Scope.
1. Phân biệt Validate và Sanitize: Đừng "ngáo" khái niệm
Nhiều anh em thường gom chung hai khái niệm này, nhưng bản chất chúng hoàn toàn khác nhau. Hãy tưởng tượng API của bạn là một quán Bar:
-
Validation (Kiểm tra hợp lệ): Là anh bảo vệ ngoài cửa kiểm tra CCCD. Anh ta chỉ trả lời "Có" hoặc "Không". (Đủ 18 tuổi thì vào, không thì cút).
-
Ví dụ: Email có đúng định dạng không? Tuổi có phải là số nguyên dương không? Mật khẩu có đủ 8 ký tự không?
-
Hành động: Nếu sai, ném ngay lỗi
400 Bad Requestvào mặt user, không lằng nhằng. -
Sanitization (Làm sạch/Khử trùng): Là việc tước vũ khí của khách trước khi cho vào trong. Bạn chấp nhận data đó, nhưng phải biến nó thành an toàn.
-
Ví dụ: User nhập tên là
<b>Hoàng</b>. Bạn không báo lỗi, nhưng bạn sanitize nó thànhHoàng(cắt bỏ HTML tags) trước khi lưu vào DB để chống XSS. Hoặc ép kiểu dữ liệu từ chuỗi"123"sang số nguyên123.
2. SQL Injection: Bóng ma quá khứ và cách tiêu diệt gọn
Dù năm nay là 2026, các ORM (Object-Relational Mapping) đã cực kỳ thông minh, nhưng SQL Injection vẫn sống thọ nếu bạn code ẩu, đặc biệt là khi viết Raw SQL để tối ưu report phức tạp.
Đoạn code "đi vào lòng đất":
// Đừng bao giờ làm thế này!
const query = `SELECT * FROM users WHERE username = '${req.query.username}'`;
db.execute(query);
Nếu username là ' OR 1=1 --, câu SQL biến thành:
SELECT * FROM users WHERE username = '' OR 1=1 --' (Lấy toàn bộ user trong hệ thống).
Giải pháp (Vũ khí tối thượng): Prepared Statements / Parameter Binding
Hãy để thư viện Database lo việc map dữ liệu. Ký tự ' sẽ bị thư viện tự động escape.
// Chuẩn Vibe Coder
const query = `SELECT * FROM users WHERE username = ?`;
db.execute(query, [req.query.username]);
3. Hiểm họa ngầm: Dynamic Sorting và bài toán Giới hạn Scope (Limit Filter Scope)
ORM có thể cứu bạn khỏi SQLi ở mệnh đề WHERE, nhưng có một điểm mù mà rất nhiều anh em chết đứng: Mệnh đề ORDER BY hoặc tên cột động (Dynamic Column Names).
Bạn không thể dùng Parameter Binding ? cho tên cột.
Sai lầm phổ biến:
// req.query.sort_by có thể là bất cứ thứ gì user nhập
const sortBy = req.query.sort_by;
const query = `SELECT id, name FROM users ORDER BY ${sortBy} DESC`;
Hacker lanh trí gửi lên: ?sort_by=password hoặc ?sort_by=secret_token. Dù hệ thống không sập, Hacker có thể dựa vào thứ tự trả về để dò đoán ra mật khẩu hoặc token (Kỹ thuật Blind SQLi).
Giải pháp: Tấm khiên Allow-list (Danh sách trắng)
Giới hạn Scope của Filter nghĩa là: Chỉ cho phép user thao tác trên những cột mà mình chỉ định rõ ràng. Mọi thứ nằm ngoài danh sách đều bị coi là rác.
// 1. Định nghĩa rõ những cột được phép sort
const ALLOWED_SORT_COLUMNS = ['created_at', 'name', 'price'];
let sortBy = req.query.sort_by;
// 2. Validate scope
if (!ALLOWED_SORT_COLUMNS.includes(sortBy)) {
// Nếu user nhập láo, một là quăng 400, hai là set về default
sortBy = 'created_at';
}
// 3. Lúc này mới an tâm nhét vào query
const query = `SELECT id, name FROM users ORDER BY ${sortBy} DESC`;
Với pattern Allow-list này, dù user có cố tình tiêm nhiễm mã độc hay tìm cách chọc ngoáy vào cột password, hệ thống của bạn vẫn trơ như đá.
4. Lời kết: Tư duy phòng thủ nhiều lớp
Một Vibe Coder thiết kế hệ thống như một pháo đài:
- Lớp ngoài cùng (Route/Middleware): Dùng các thư viện validation (như Zod, Joi trong Nodejs hoặc Validator trong Golang) để chặn đứng mọi payload sai định dạng.
- Lớp giữa (Controller/Service): Sanitize dữ liệu (ép kiểu, trim khoảng trắng, xóa thẻ HTML) và Giới hạn Scope cho các dynamic query (chỉ dùng Allow-list).
- Lớp trong cùng (Database): Luôn dùng Parameterized Queries và thiết lập quyền DB User ở mức tối thiểu (Least Privilege - App chỉ được SELECT/INSERT/UPDATE, cấm DROP TABLE).
Bảo mật không phải là việc làm sau cùng. Nó phải nằm trong tư duy (mindset) ngay từ dòng code đầu tiên.
Chủ đề tiếp theo: Kẻ Thù Số 1 Của Public API - Bot Cào Data & Đòn Trừng Phạt Từ Rate Limiting
Bạn đã bảo vệ API khỏi hacker xâm nhập, nhưng nếu đối thủ cạnh tranh không hack mà họ cử một con Bot gọi API tìm kiếm của bạn 1000 lần/giây để cào hết sạch dữ liệu sản phẩm, giá bán về trang của họ thì sao? Chẳng mấy chốc Server của bạn cũng sập vì cạn kiệt tài nguyên.
Ở bài viết tới, mình sẽ chia sẻ về "nghệ thuật từ chối": Rate Limiting và Throttling. Làm sao để nhận diện một thằng spammer và khóa mõm nó lại (HTTP 429 Too Many Requests) bằng Redis mà không làm ảnh hưởng đến trải nghiệm của user thật? Anh em nhớ follow và đón đọc nhé!
All rights reserved