Loạt bài phòng thủ: hiểu lỗ hổng để viết code an toàn, demo trong lab cô lập.
Ta đã thấy SQL injection (phần 1) và XSS (phần 4) — hai biến thể của cùng một lỗi gốc: input người dùng biến thành code. Bài này gộp hai lỗ hổng nữa cùng họ, cùng một gốc rễ, cùng một kiểu phòng thủ.
Command injection xảy ra khi ứng dụng dựng một lệnh hệ điều hành bằng cách ghép input người dùng vào, rồi chạy qua shell. Shell có các ký tự đặc biệt (;, |, &&, `) để nối/điều khiển lệnh — nên một input chứa ; rm -rf ... biến một lệnh thành hai. Path traversal xảy ra khi ứng dụng mở một file dựa trên tên do người dùng cung cấp, mà không kiểm — input ../../../etc/passwd dùng .. để leo ra khỏi thư mục cho phép, đọc file bất kỳ trên hệ thống. Bài này (phần 7 loạt Bảo mật web) tái hiện cả hai và cách chặn.
Cơ chế: input thành code, hoặc vượt ranh giới

Hình 1: Command injection — dựng lệnh qua shell bằng nối chuỗi (sh -c "ping "+input), input chứa ; làm shell chạy lệnh thứ hai; chặn bằng exec.Command("ping", input) tách tham số không qua shell. Path traversal — nối đường dẫn thô (filepath.Join(baseDir, userFile)) với ../ thoát ra ngoài; chặn bằng filepath.Clean + kiểm HasPrefix(clean, baseDir).
Tái hiện thật trong go-lab (lab cô lập)
Mình chạy trong go-lab (golang 1.23) với input độc giả định, trong một thư mục lab cô lập.

Hình 2: Kết quả thật — A. command injection: shell nối chuỗi chạy cả lệnh thứ hai (CHIEM_QUYEN_THANH_CONG), còn tách tham số coi input là một text (ping localhost; echo ... không chạy); B. path traversal: nối thô đọc được file bí mật ngoài baseDir, còn Clean + kiểm thuộc baseDir thì TỪ CHỐI (vẫn cho đọc public.txt).
Kết quả rõ ràng:
- Command injection: shell biến một lệnh thành hai. Khi dựng lệnh bằng
sh -c "ping " + inputvới inputlocalhost; echo CHIEM_QUYEN..., output cho thấy cả hai lệnh chạy:ping localhostrồiCHIEM_QUYEN_THANH_CONG. Dấu;được shell diễn giải là "kết thúc lệnh, bắt đầu lệnh mới". Kẻ tấn công chạy lệnh tuỳ ý trên server (; rm,; catfile nhạy cảm,; curlgửi dữ liệu ra ngoài). Ngược lại,exec.Command("echo", "ping", input)tách tham số — không có shell diễn giải, nên inputlocalhost; echo CHIEM_QUYEN...chỉ là một đối số text, in ra nguyên văn, không có lệnh nào chạy thêm. - Path traversal:
..leo ra ngoài thư mục. Khi mở file bằngfilepath.Join(baseDir, userFile)thô, input../SECRET.txtthoát khỏi baseDir và đọc đượcSECRET.txtnằm ngoài — nội dung file bí mật lộ ra. Với phòng thủ (filepath.Cleanđể chuẩn hoá.., rồi kiểm đường dẫn kết quả vẫn thuộc baseDir), request../SECRET.txtbị TỪ CHỐI, trong khi file hợp lệpublic.txtvẫn đọc được bình thường.
Cả hai lỗ hổng, như SQL injection và XSS, đều cùng một bệnh: trộn lẫn dữ liệu người dùng với code/ranh giới hệ thống. Và cùng một thuốc: tách bạch chúng.
Đánh đổi cần cân nhắc
Đừng dựng lệnh shell từ input — dùng API tách tham số. Quy tắc số một cho command injection: tránh shell hoàn toàn khi có thể. Dùng API thực thi tách tham số (exec.Command(prog, arg1, arg2) trong Go, subprocess.run([...]) không shell=True trong Python) — chúng truyền đối số trực tiếp cho chương trình, không qua shell diễn giải, nên input không bao giờ thành lệnh. Nếu bắt buộc phải dùng shell (hiếm — ví dụ cần pipe), thì input phải qua allowlist nghiêm ngặt (chỉ cho ký tự an toàn đã biết), không bao giờ nối thẳng. Và tốt hơn nữa: nếu có thư viện làm việc đó (ví dụ gọi API thay vì chạy lệnh curl), dùng thư viện thay vì gọi lệnh ngoài.
Path: luôn Clean + kiểm thuộc baseDir, và cẩn thận các biến thể. filepath.Clean chuẩn hoá .. và ., nhưng chỉ Clean là chưa đủ — phải kiểm đường dẫn kết quả có nằm trong baseDir không (như demo). Cẩn thận các biến thể vượt qua kiểm tra ngây thơ: .. được URL-encode (%2e%2e), đường dẫn tuyệt đối (/etc/passwd), symlink trỏ ra ngoài, hay (trên Windows) dấu \. Go 1.24+ có os.Root cho phép mở file bị giới hạn trong một gốc (an toàn hơn kiểm tay); nếu không có, có thể dùng chroot/container. Nguyên tắc: đừng tin tên file từ người dùng — kiểm nó thuộc vùng cho phép trước khi mở.
Phòng thủ theo lớp: least privilege và cô lập. Dù code đúng, hãy giảm thiểu thiệt hại nếu có lỗ hổng lọt. Chạy ứng dụng với quyền tối thiểu (không root), trong container/sandbox giới hạn file và network truy cập được, với filesystem chỉ-đọc ở chỗ không cần ghi. Nếu một command injection lọt qua, một tiến trình quyền thấp trong container cô lập gây hại ít hơn nhiều so với một tiến trình root trên host. Code đúng là tuyến đầu; cô lập là lưới an toàn khi tuyến đầu thủng.
Ba ý mang về
- Command injection: input thành lệnh khi dựng lệnh qua shell: tái hiện thật,
sh -c "ping "+inputvới input chứa;chạy cả lệnh thứ hai (CHIEM_QUYEN_THANH_CONG);exec.Command("ping", input)tách tham số coi input là một đối số text, không chèn được lệnh — tránh shell là chìa khoá. - Path traversal:
..leo ra khỏi thư mục cho phép: tái hiện thật, nối đường dẫn thô với../SECRET.txtđọc được file bí mật ngoài baseDir;filepath.Clean+ kiểmHasPrefix(clean, baseDir)từ chối đường dẫn thoát ra, vẫn cho đọc file hợp lệ. - Cùng gốc, cùng thuốc — và phòng thủ theo lớp: cả hai (như SQL injection/XSS) là input biến thành code/vượt ranh giới; chặn bằng tách tham số (không shell, allowlist nếu buộc) và làm sạch + kiểm đường dẫn (cẩn thận encode/symlink, dùng
os.Root); cộng least privilege + container cô lập làm lưới an toàn.
Nguồn
- OWASP — Command Injection: https://owasp.org/www-community/attacks/Command_Injection
- OWASP — Path Traversal: https://owasp.org/www-community/attacks/Path_Traversal
- Go — os/exec (chạy lệnh không qua shell): https://pkg.go.dev/os/exec
Phần sau ta bàn về chống dò mật khẩu: rate limiting và account lockout cho đăng nhập — đo thật cách giới hạn số lần thử làm brute-force bất khả thi, và các đánh đổi (DoS, khoá oan người dùng thật).