Kebutuhan yang sama muncul berulang di banyak aplikasi: memilih operasi kalkulator dari tombol yang ditekan, membaca konfigurasi yang dikirim server dalam bentuk teks, membaca properti bertingkat dari jalur seperti tampilan.tema, atau menyimpan pilihan pengguna sebagai string lalu memakainya kembali. Di titik ini banyak orang menemukan eval — function yang menerima string dan menjalankannya sebagai kode JavaScript. Satu baris, semua selesai; kelihatannya seperti kunci untuk semua kunci.

Masalahnya persis di satu kata itu: string apa pun menjadi kode. Bila string itu pernah menyentuh input pengguna, kita menjalankan program tulisan orang yang tidak kita kenal — dengan semua hak kode halaman kita. Chapter ini menunjukkan mengapa eval harus dihindari dan membuktikan bahwa dua kebutuhan paling umum di atas punya pengganti yang lebih aman sekaligus lebih sederhana: object pemetaan dan JSON.parse.

Apa yang Dilakukan eval #

Bentuk paling ringkas dari bahayanya:

js Ilustrasi — jangan dijalankan
const pilihanPengguna = 'alert("Halo dari string")';
eval(pilihanPengguna); // string menjadi program

Jika dijalankan (ilustrasi):

text
Browser menampilkan dialog alert "Halo dari string". Bila isi string
diganti kode lain — misalnya membaca cookie atau mengirim data ke
server pihak lain — kode itu berjalan dengan hak yang sama seperti
kode halaman kita.

Mekanismenya: saat eval dipanggil, engine mengalirkan string ke parser JavaScript dan menjalankan hasilnya di tempat eval dipanggil — dengan akses ke seluruh variabel di lingkup sekitarnya. Saudaranya, new Function("...", "..."), mengompilasi string menjadi function dengan lingkup global. Keduanya membuka risiko yang sama:

  • String dari input adalah program dari orang asing. Parser tidak membedakan string yang kita tulis dan string yang diketik pengunjung.
  • Kode menjadi tidak terbaca alat. Bundler, minifier, dan linter menganalisis kode yang tertulis; isi string tidak mereka proses, sehingga eval juga jadi lubang bagi optimisasi dan pemeriksaan.
  • Kebijakan CSP memblokirnya. Kebijakan yang tidak mencantumkan 'unsafe-eval' — dibahas di chapter Content Security Policy Overview — akan menghentikan eval di browser.
  • Bentuk tersembunyi sama berbahayanya. setTimeout("kode", 1000) dengan argumen string juga menjalankan string sebagai kode — gunakan bentuk function, setTimeout(() => ..., 1000).
  • Sulit didebug. Galat di dalam string muncul tanpa konteks baris yang bermakna di berkas asal, dan stack trace menunjuk ke lokasi yang tidak bisa dibuka editor.

Pola yang menyatukan keempat butir itu: eval menghapus batas antara data dan program. Setelah batas itu hilang, tidak ada alat — kompiler, linter, maupun review — yang bisa membantu kita memastikan apa yang benar-benar dijalankan.

Alternatif: Object Pemetaan dan JSON.parse #

Dua kebutuhan yang paling sering menggoda orang memakai eval — memilih operasi dari nama, dan membaca data dari teks — keduanya punya jalur yang tidak mengeksekusi apa pun. Berkas berikut menguji keduanya di terminal:

// peta-operasi.js — mengganti eval dengan object pemetaan dan JSON.parse
const operasi = new Map([
  ["tambah", (a, b) => a + b],
  ["kurang", (a, b) => a - b],
  ["kali", (a, b) => a * b],
  ["bagi", (a, b) => (b === 0 ? null : a / b)],
]);

function hitung(nama, a, b) {
  const fungsi = operasi.get(nama);
  if (fungsi === undefined) {
    return { ok: false, alasan: `operasi "${nama}" tidak dikenal` };
  }
  return { ok: true, hasil: fungsi(a, b) };
}

// 1. permintaan sah — misalnya tombol yang ditekan pengguna
console.log(hitung("tambah", 12, 8));
console.log(hitung("kali", 12, 8));

// 2. permintaan berisi kode, bukan nama operasi: ditolak apa adanya
console.log(hitung('alert("Halo")', 12, 8));

// 3. bracket lookup pada object biasa punya jebakan warisan
const petaSederhana = { tambah: (a, b) => a + b };
console.log(typeof petaSederhana["constructor"]);

// 4. data terstruktur dibaca dengan JSON.parse, bukan dieksekusi
function bacaKonfigurasi(teks) {
  try {
    const data = JSON.parse(teks);
    return { ok: true, data };
  } catch {
    return { ok: false, alasan: "bukan JSON yang sah" };
  }
}
console.log(bacaKonfigurasi('{"tema":"gelap","ukuran":14}'));
console.log(bacaKonfigurasi("tema gelap; alert(1)"));

Output yang Dihasilkan:

text
{ ok: true, hasil: 20 }
{ ok: true, hasil: 96 }
{ ok: false, alasan: 'operasi "alert("Halo")" tidak dikenal' }
function
{ ok: true, data: { tema: 'gelap', ukuran: 14 } }
{ ok: false, alasan: 'bukan JSON yang sah' }

Mari kita bedah baris per baris:

  1. Baris 2–7 (object pemetaan): Map memasangkan nama operasi dengan function yang sudah kita tulis dan kita pahami. Tidak ada string yang dikompilasi — yang ada hanya pencarian kunci.
  2. Baris 9–15 (function hitung): operasi.get(nama) mengembalikan undefined untuk kunci mana pun yang tidak terdaftar, dan permintaan seperti alert("Halo") jatuh ke cabang penolakan. String dari pengguna dipakai sebagai kunci, tidak pernah sebagai kode.
  3. Baris 22 (permintaan berisi kode): keluaran ketiga memperlihatkan penolakan yang tenang — { ok: false, ... } — bukan dialog yang terbuka. Bandingkan dengan ilustrasi eval di atas: input yang sama, akibat yang berbeda total.
  4. Baris 25–26 (jebakan bracket lookup): bila pemetaan memakai object biasa, petaSederhana["constructor"] ternyata mengembalikan function — properti warisan dari Object.prototype yang terbawa lewat rantai prototype (chapter Prototype Chain). Dua penangkalnya: gunakan Map seperti baris 2, atau saring dengan Object.hasOwn(petaSederhana, nama) sebelum membaca.
  5. Baris 29–36 (bacaKonfigurasi): JSON.parse membaca teks dengan aturan yang ketat — tata bahasa JSON hanya bisa mengekspresikan data (object, array, string, angka, boolean, null), tidak ada instruksi yang bisa dieksekusi. String rusak ditangkap catch dan dilaporkan, bukan dijalankan.
  6. Baris 37–38 (dua uji): konfigurasi sah kembali sebagai object ({ ok: true, data: ... }), sedangkan teks yang mencampur kode ditolak dengan pesan bukan JSON yang sah — bukan dieksekusi.

Bracket Lookup Aman untuk Jalur Properti #

Kebutuhan klasik yang tersisa: membaca properti bertingkat memakai teks jalur seperti keranjang.pajak — jalur itu bisa datang dari pilihan menu, parameter URL, atau pesan server. Jalan pintas yang sering muncul adalah eval("konfigurasi." + jalur); penggantinya adalah menyusuri jalur dengan pemisahan titik, sambil menjaga setiap tingkat:

js Bracket lookup aman: menyusuri jalur properti
const KONFIGURASI = {
  keranjang: { pajak: 0.11, kupon: null },
  tampilan: { tema: "gelap" },
};

function bacaPath(objek, jalur) {
  let nilai = objek;
  for (const kunci of jalur.split(".")) {
    if (nilai === null || typeof nilai !== "object") return undefined;
    if (!Object.hasOwn(nilai, kunci)) return undefined;
    nilai = nilai[kunci];
  }
  return nilai;
}

console.log(bacaPath(KONFIGURASI, "keranjang.pajak"));
console.log(bacaPath(KONFIGURASI, "tampilan.tema"));
console.log(bacaPath(KONFIGURASI, "keranjang.__proto__"));
console.log(bacaPath(KONFIGURASI, "tampilan.tema.warna"));

Output yang Dihasilkan:

text
0.11
gelap
undefined
undefined

Bedah singkat:

  1. jalur.split("."): teks jalur dipecah menjadi daftar kunci. Jalur diperlakukan sebagai data yang dibaca satu per satu — bukan potongan kode yang dikompilasi.
  2. Pemeriksaan typeof dan null: bila tingkat antara bukan object (misalnya jalur lanjut melewati string), penyusuran berhenti dan mengembalikan undefined — bukan galat yang tidak tertangani.
  3. Object.hasOwn(nilai, kunci): hanya properti milik object sendiri yang boleh diikuti. Uji ketiga membuktikannya: kunci warisan seperti __proto__ ditolak, meskipun lookup biasa keranjang["__proto__"] akan mengembalikan object prototype (chapter Prototype Chain).
  4. Jalur yang berhenti di tengah: uji keempat mencoba melanjutkan dari sebuah string; hasilnya undefined yang aman untuk dibaca pemanggil.

Pola yang sama berlaku untuk kebutuhan lain yang kelihatannya butuh “kode dinamis”. Tabel ini merangkum penggantinya:

Kebutuhan yang tampak butuh kode dinamisPengganti yang tepat
Memilih operasi atau aksi dari namaMap nama → function (berkas utama di atas)
Membaca data terstruktur dari teksJSON.parse di dalam try...catch
Membaca properti dari jalur teksbacaPath seperti di atas
Menunda eksekusisetTimeout(() => ..., ms) dengan bentuk function
Merender teks dengan isi dinamistemplate literal + escapeHtml (chapter Safe DOM Updates)

Setelah beberapa kali menulis penggantinya, pola ini terasa natural: string dari luar selalu berakhir sebagai data — kunci Map, hasil JSON.parse, atau daftar kunci jalur — dan hanya kode yang kita tulis sendiri yang pernah dijalankan. Untuk kebutuhan merender teks pada skala besar, pustaka template engine pun bekerja dengan parsing dan data binding, bukan dengan eval; pilih pustaka, jangan rakit parser sendiri.

Alat berikutnya juga menerima string dari pengguna, tetapi dengan konteks yang berbeda: teks yang dijanjikan berupa URL.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

peta-operasi.js

Unduh berkas
javascript
// peta-operasi.js — mengganti eval dengan object pemetaan dan JSON.parse
const operasi = new Map([
  ["tambah", (a, b) => a + b],
  ["kurang", (a, b) => a - b],
  ["kali", (a, b) => a * b],
  ["bagi", (a, b) => (b === 0 ? null : a / b)],
]);

function hitung(nama, a, b) {
  const fungsi = operasi.get(nama);
  if (fungsi === undefined) {
    return { ok: false, alasan: `operasi "${nama}" tidak dikenal` };
  }
  return { ok: true, hasil: fungsi(a, b) };
}

// 1. permintaan sah — misalnya tombol yang ditekan pengguna
console.log(hitung("tambah", 12, 8));
console.log(hitung("kali", 12, 8));

// 2. permintaan berisi kode, bukan nama operasi: ditolak apa adanya
console.log(hitung('alert("Halo")', 12, 8));

// 3. bracket lookup pada object biasa punya jebakan warisan
const petaSederhana = { tambah: (a, b) => a + b };
console.log(typeof petaSederhana["constructor"]);

// 4. data terstruktur dibaca dengan JSON.parse, bukan dieksekusi
function bacaKonfigurasi(teks) {
  try {
    const data = JSON.parse(teks);
    return { ok: true, data };
  } catch {
    return { ok: false, alasan: "bukan JSON yang sah" };
  }
}
console.log(bacaKonfigurasi('{"tema":"gelap","ukuran":14}'));
console.log(bacaKonfigurasi("tema gelap; alert(1)"));