Cách trả lời tốt là đi theo đường đi của dữ liệu, không đọc thuộc danh sách lỗ hổng.
1. Vào — mọi input đều không tin được.
- Validate bằng schema ở phía server (client-side chỉ là trải nghiệm). Whitelist field, ép kiểu, giới hạn độ dài.
- Query DB bằng tham số hoá, không ghép chuỗi. Lệnh hệ thống thì tránh shell, truyền mảng tham số.
- Input là URL hay đường dẫn file → kiểm tra theo mục SSRF / path traversal.
2. Quyền — chỗ hay hổng nhất trong code mới.
- Mỗi endpoint hỏi hai câu: đã đăng nhập chưa và có quyền trên đúng bản ghi này không. Thiếu vế sau là IDOR — đổi /orders/123 thành /orders/124 là đọc được đơn người khác.
- Kiểm tra quyền ở tầng dữ liệu (điều kiện WHERE owner_id = ?), không chỉ ở UI hay route guard.
- Kiểm soát mass assignment: không cho phép client gửi thẳng role, isAdmin, balance vào bản ghi.
3. Ra — mọi output đều có ngữ cảnh.
- Escape theo ngữ cảnh render; HTML của user thì sanitize khi render.
- Response chỉ chứa field cần thiết — đừng trả nguyên object user rồi để frontend tự ẩn.
- Lỗi trả về chung chung, chi tiết vào log nội bộ.
4. Cấu hình và hạ tầng.
- Cookie phiên: HttpOnly, Secure, SameSite. Header: HSTS, nosniff, frame-ancestors, CSP.
- Secret nằm trong secret manager, không trong repo; kiểm tra không có key nào lọt vào bundle client (NEXT_PUBLIC_*).
- Rate limit cho endpoint đăng nhập, OTP, gửi mail, và các endpoint tốn tài nguyên.
- Migration/feature flag có đường lùi; job nền và webhook cũng phải xác thực.
5. Trước khi merge.
- Chạy pnpm audit / Dependabot cho dependency mới thêm.
- Viết test cho đường thất bại: gọi endpoint bằng user khác, không token, token hết hạn.
- Tự hỏi: nếu tính năng này bị lạm dụng ở quy mô lớn thì hậu quả là gì — thường lộ ra vấn đề rate limit và chi phí.