Command injection terjadi kalau aplikasi menyusun perintah terminal (shell command) dari input user, lalu shell membaca sebagian input itu sebagai perintah tambahan. Bayangin kamu nitip pesan ke kurir: "antar paket ke alamat ___". Kalau isian alamatnya ditulis "Jl. Merdeka 5, lalu ambil juga kunci gudang", kurir yang patuh bakal menjalankan dua tugas. Shell adalah kurir yang patuh itu, dan beberapa tanda baca baginya berarti "lalu kerjakan ini juga".
🧪 Tebakan cepat: server menjalankan
exec("ping -c 1 " + host)dan inputhostberisi8.8.8.8; ls. Berapa perintah yang dijalankan shell? Tebak dulu, jawabannya ada di bagian "Kenapa shell menjalankan dua perintah".
Kenapa shell menjalankan dua perintah
Misalnya ada fitur "cek koneksi" yang menjalankan ping ke host yang diketik user:
// RENTAN: exec menjalankan satu string lewat shell
const { exec } = require("child_process");
app.get("/ping", (req, res) => {
const host = req.query.host;
exec(`ping -c 1 ${host}`, (err, stdout) => res.send(stdout));
});
execmengirim satu baris teks utuh ke shell, persis seperti kamu mengetiknya di terminal.- Dengan host
8.8.8.8, baris itu jadiping -c 1 8.8.8.8. Satu perintah, aman. - Dengan host
8.8.8.8; ls, baris itu jadiping -c 1 8.8.8.8; ls. Bagi shell, titik koma;artinya "perintah selesai, jalankan yang berikutnya". Jadi shell menjalankanping, laluls(menampilkan isi folder server), dan hasil keduanya dikirim ke user.
Itu jawaban tebakannya: dua perintah. Input user berubah jadi perintah karena dicampur ke teks yang dibaca shell. Polanya sama dengan SQL injection, cuma "bahasanya" shell.
Tanda yang punya arti khusus buat shell antara lain ; (lanjut ke perintah berikutnya), && dan || (lanjut kalau berhasil atau gagal), | (alirkan hasil ke perintah lain), dan $( ) (jalankan perintah di dalamnya dulu). Kamu gak perlu hafal semuanya, cukup ingat: teks dari user gak boleh dibaca shell.
execFile: tanpa shell, argumen terpisah
Perbaikan utamanya: jangan lewat shell sama sekali. execFile menjalankan satu program langsung, dan argumennya dikirim sebagai array, satu kotak per argumen:
// AMAN: execFile tanpa shell, host jadi satu argumen
const { execFile } = require("child_process");
execFile("ping", ["-c", "1", host], (err, stdout) => res.send(stdout));
- Programnya
ping, argumennya tiga:-c,1, dan isihost. - Dengan host
8.8.8.8; ls,pingmenerima satu argumen berisi teks8.8.8.8; lsutuh. Gak ada shell yang membaca titik komanya, jadilsgak pernah jalan. pingcuma bingung: gak ada host bernama begitu, jadi dia gagal dengan pesan error. Itu hasil yang kita mau.
Allowlist: tolak input yang bentuknya salah
Lapisan kedua: periksa input terhadap bentuk yang diizinkan sebelum menjalankan apa pun. Nama host cuma butuh huruf, angka, titik, dan tanda minus:
const HOST = /^[a-z0-9.-]+$/i;
if (!HOST.test(host)) {
return res.status(400).send("Host tidak valid");
}
execFile("ping", ["-c", "1", host], (err, stdout) => res.send(stdout));
8.8.8.8 lolos, 8.8.8.8; ls ditolak dengan status 400 karena spasi dan titik koma gak ada di daftar. Kalau pilihannya memang sedikit (misalnya tiga server tetap), lebih rapat lagi: cocokkan ke daftar nama persis dengan ["google.com", "cloudflare.com"].includes(host).
Kalau terpaksa lewat shell: escapeshellarg
Di PHP, exec() dan shell_exec() selalu lewat shell. Kalau terpaksa, bungkus tiap input dengan escapeshellarg():
$safe = escapeshellarg($userInput);
exec("ping -c 1 " . $safe);
escapeshellarg() membungkus input dengan kutip satu dan meng-escape kutip di dalamnya, jadi shell melihat '8.8.8.8; ls' sebagai satu argumen, bukan dua perintah. Tetap saja, pilihan terbaik adalah library yang gak butuh shell (misalnya sharp untuk mengolah gambar, bukan menjalankan convert).
⚠️ Jebakan yang sering ketemu
- Cuma memblokir
;. Shell punya banyak tanda lain (&&,|,$( )). Daftar larangan selalu ketinggalan satu; pakai execFile dan allowlist. shell: truedi execFile atau spawn. Opsi itu menyalakan shell lagi, dan argumen digabung jadi satu teks. Biarkan default-nya (tanpa shell).escapeshellcmd()dikira sama denganescapeshellarg().escapeshellcmd()meng-escape satu baris perintah utuh dan lebih longgar. Untuk satu nilai dari user, pakaiescapeshellarg().- Percaya input dari dropdown atau field tersembunyi. User bisa mengirim request apa pun. Validasi di server tetap wajib.
TL;DR: Command injection terjadi kalau input user dicampur ke teks yang dibaca shell: dengan exec("ping -c 1 " + host) dan host 8.8.8.8; ls, titik koma membuat shell menjalankan dua perintah, ping lalu ls. Cegah dengan execFile("ping", ["-c", "1", host]): tanpa shell, host jadi satu argumen utuh. Tambahkan allowlist yang menolak input berbentuk salah dengan status 400. Kalau terpaksa lewat shell di PHP, bungkus input dengan escapeshellarg(), dan lebih baik lagi pakai library yang gak butuh shell.