Command Injection — Security

Apa itu Command Injection? Command Injection terjadi ketika aplikasi menjalankan perintah sistem operasi dengan menyertakan input pengguna tanpa sanitasi…

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 input host berisi 8.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));
});

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));

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

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.

Yang akan kamu pelajari