Path traversal xảy ra khi tên file/đường dẫn lấy từ input của user rồi ghép vào thao tác đọc/ghi file.
// vulnerable
app.get('/download', (req, res) => {
res.sendFile('/var/data/' + req.query.name)
})
// ?name=../../../../etc/passwdVì sao lọc ../ không đủ:
- Lọc một lần, không lặp: ....// sau khi bỏ ../ còn lại ../.
- Biến thể encode: %2e%2e%2f, double-encode %252e%252e%252f, overlong UTF-8, backslash ..\ trên Windows.
- Absolute path hoàn toàn không chứa ../: ?name=/etc/passwd với hàm join có thể bỏ qua prefix.
- Symlink trỏ ra ngoài thư mục cho phép.
Blacklist luôn thua ở cuộc đua biến thể encoding.
Cách đúng — không lọc mà xác minh kết quả:
1. Tốt nhất: không nhận đường dẫn. Nhận một id, tra DB lấy đường dẫn thật đã kiểm soát.
2. Nếu buộc phải nhận tên, normalize rồi kiểm tra prefix:
const path = require('node:path')
const ROOT = '/var/data'
const full = path.resolve(ROOT, req.query.name)
if (full !== ROOT && !full.startsWith(ROOT + path.sep)) {
return res.status(400).end()
}Điểm mấu chốt: resolve trước, so sánh sau. Lúc đó mọi biến thể ../ hay encode đã bị gộp về một đường dẫn tuyệt đối duy nhất để đối chiếu.
3. Thêm allowlist ký tự cho tên file (chữ, số, -, _, .), và kiểm quyền ở tầng OS (process chạy bằng user chỉ đọc được thư mục cần thiết).