Ba câu hỏi phải trả lời: file có đúng loại không, lưu ở đâu, và phục vụ lại ra sao.
1. Kiểm tra loại thật, không tin client.
- Phần mở rộng và Content-Type do client gửi đều sửa được. evil.php đổi tên thành avatar.jpg vẫn qua.
- Đọc magic bytes đầu file (FF D8 FF cho JPEG, 89 50 4E 47 cho PNG) hoặc dùng thư viện detect kiểu.
- Chắc chắn nhất với ảnh: re-encode qua thư viện xử lý ảnh (sharp, ImageMagick). File polyglot vừa là ảnh vừa là script sẽ bị phá khi encode lại.
2. Tên file — không bao giờ dùng tên gốc.
- Sinh tên mới (UUID) + phần mở rộng suy ra từ loại đã kiểm tra. Cách này diệt luôn path traversal (../../etc/passwd), null byte (a.php\0.jpg), tên trùng lặp và ký tự Unicode gây rối.
- Lưu tên gốc vào DB nếu cần hiển thị, và escape khi render.
3. Nơi lưu và cách phục vụ.
- Không lưu trong web root. Tốt nhất là object storage (S3/R2) ở bucket riêng, hoặc thư mục ngoài docroot rồi stream qua ứng dụng.
- Phục vụ từ domain khác (sandbox domain) để file không chạy trong origin chính.
- Đặt Content-Type tường minh theo loại đã kiểm tra, kèm X-Content-Type-Options: nosniff và Content-Disposition: attachment cho file không phải ảnh.
- Vô hiệu hoá thực thi ở thư mục upload (không map PHP/CGI handler).
4. Giới hạn và quét. Giới hạn dung lượng ở cả reverse proxy và ứng dụng (chống DoS), giới hạn số lần upload theo user, quét virus nếu là file tài liệu.
Riêng SVG: là XML, chứa được <script> và onload. Hoặc từ chối SVG, hoặc sanitize bằng DOMPurify và phục vụ ở domain sandbox.