Chapter sebelumnya menutup dengan bukti yang tidak nyaman: satu penugasan innerHTML cukup untuk mengubah ketikan pengunjung menjadi node di halaman kita. Kabar baiknya, kebiasaan yang menutup celah itu sederhana dan murah — hanya butuh dipilih secara sadar setiap kali kita mengubah isi DOM, karena pilihan yang salah justru paling praktis saat sedang tergesa.

Aturan mainnya tiga lapis: textContent sebagai pilihan bawaan untuk teks; escapeHtml saat markup milik kita perlu membungkus data dari luar; dan pustaka sanitizer seperti DOMPurify saat markup kaya dari pihak lain memang harus ditampilkan. Chapter DOM sebelumnya sudah memperlihatkan dua jalur ini bekerja di jsdom; chapter ini menstandarkan aturannya dan menguji lapisan kedua langsung di terminal.

Aturan Bawaan: textContent Lebih Dulu #

Mekanisme di balik perbedaan keduanya satu kalimat: textContent tidak pernah memanggil parser HTML, sedangkan innerHTML selalu memanggilnya. String yang ditulis lewat textContent disimpan sebagai teks node — parser tidak pernah terlibat, sehingga tidak ada peluang untuk membentuk elemen, atribut, atau entitas palsu. Efek samping positifnya, penulisan lewat textContent juga lebih cepat karena pekerjaan parsing itu tidak ada.

Karena itu aturan bawaannya: nama pengguna, komentar, judul, angka, hasil pencarian — semuanya adalah teks, dan teks ditampilkan lewat textContent. innerHTML baru dipakai setelah ada jawaban eksplisit atas pertanyaan “markup apa yang saya butuhkan di sini?”. Perlu ditegaskan juga arah yang aman: membaca innerHTML dari node yang dibangun kode kita tidak berbahaya — bahaya selalu ada di penulisan dengan string yang tidak kita kendalikan.

Tabel berikut memetakan tiga kebutuhan yang muncul berulang-ulang:

KebutuhanAlatAlasan
Menampilkan teks dari pengguna atau pihak laintextContenttanpa parser, tidak ada peluang markup hidup
Membungkus data dari luar dengan markup milik kitatemplate literal + escapeHtml() pada tiap nilaiparser hanya melihat karakter yang sudah jinak
Menampilkan markup kaya dari pihak lainpustaka sanitizer (DOMPurify)tag dan atribut disaring sesuai daftar aman

escapeHtml: Sanitasi Dasar #

Function ini sudah kita kenal di section DOM; kali ini kita uji berdiri sendiri di terminal. Logikanya murni operasi string — tidak perlu DOM — sehingga keluarannya bisa diverifikasi persis di Node:

// escape-html.js — sanitasi dasar: teks pihak lain aman disisipkan ke markup
function escapeHtml(teks) {
  const peta = {
    "&": "&",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#39;",
  };
  return teks.replace(/[&<>"']/g, (karakter) => peta[karakter]);
}

// 1. ulasan biasa yang memuat karakter khusus
console.log(escapeHtml("Karya & pena <terbaik> \"Budi\""));

// 2. input berisi markup utuh: seluruh markup ikut menjadi teks
console.log(escapeHtml('<img src=x onerror="alert(\'Punten\')">'));

// 3. hasil escape aman dibungkus markup milik kita sendiri
const kartu = `<div class="ulasan">${escapeHtml("Diskon <b>50%</b> & gratis ongkir")}</div>`;
console.log(kartu);

// 4. pemeriksaan: tidak ada sudut kurung mentah yang tersisa
const hasil = escapeHtml("a < b & c > d");
console.log(hasil);
console.log(/[<>]/.test(hasil), hasil.includes("&lt;"), hasil.includes("&amp;"));

Output yang Dihasilkan:

text
Karya &amp; pena &lt;terbaik&gt; &quot;Budi&quot;
&lt;img src=x onerror=&quot;alert(&#39;Punten&#39;)&quot;&gt;
<div class="ulasan">Diskon &lt;b&gt;50%&lt;/b&gt; &amp; gratis ongkir</div>
a &lt; b &amp; c &gt; d
false true true

Mari kita telaah baris-baris pentingnya:

  1. Baris 3–10 (peta dan replace): lima karakter yang bermakna khusus bagi parser HTML dipetakan ke entitasnya. & ikut dipetakan karena dialah pembuka semua entitas — tanpa itu, teks yang memang berisi &amp; bisa ditafsir ulang menjadi karakter lain.
  2. Baris 10 (replace dengan callback): pola /[&<>"']/g menemukan satu karakter bahaya mana pun di seluruh string, dan callback menggantinya dengan entitas dari peta. replace bekerja dalam satu lintasan atas karakter asli — hasil penggantian tidak dipindai ulang — sehingga urutan entitas tidak saling merusak.
  3. Baris 14 (teks biasa): kutip ganda menjadi &quot; dan apostrof menjadi &#39;. Keduanya penting saat nilai akan ditempel di dalam atribut HTML yang berkutip, bukan hanya di antara tag.
  4. Baris 17 (markup utuh): seluruh tag <img ...> berubah menjadi teks yang ditampilkan apa adanya. Atribut onerror kini hanya rangkaian karakter di layar, bukan penangan peristiwa yang menunggu terpicu.
  5. Baris 20–21 (markup milik kita, data dari luar): template literal menyusun <div> milik kode kita, sementara satu-satunya nilai dari luar selalu melewati escapeHtml. Baris 26 memastikan: false berarti tidak ada sudut kurung mentah yang tersisa, dan dua true berikutnya membuktikan entitasnya benar-benar terpasang.

Sanitasi dasar ini punya batas yang harus diakui: escapeHtml mengamankan konteks teks dan atribut berkutip biasa, tetapi ia tidak mengenali tag mana yang “boleh”. Untuk nilai yang akan menjadi href (konteks URL), lapisan yang tepat adalah validasi URL — dibahas di chapter URL Validation. Untuk markup kaya, kita butuh alat yang benar-benar mengenali struktur HTML.

Dengan batas itu, penempatan pemanggilannya pun jelas: escape dilakukan tepat sebelum nilai masuk ke markup — di satu tempat, satu kali. String yang sudah ter-escape tidak perlu di-escape ulang (dan sebaiknya tidak, agar &amp; tidak berubah menjadi &amp;amp;), sementara string mentah tidak boleh menyusup melewati titik itu tanpa perlakuan. Satu titik jalan keluar yang disiplin jauh lebih mudah diaudit daripada escape yang tersebar di lima tempat.

Saat Markup Kaya Dibutuhkan: DOMPurify #

Pratinjau artikel, komentar berformat, dan konten serupa memang berupa HTML dari pihak lain. Menyaring string seperti itu dengan replace buatan sendiri adalah jalan yang licin: parser HTML punya banyak kejutan, dan daftar tag serta atribut yang perlu ditangani terlalu panjang untuk satu function kecil. Di sinilah pustaka sanitizer masuk — yang paling banyak dipakai adalah DOMPurify:

js Ilustrasi: pustaka sanitizer DOMPurify
const bersih = DOMPurify.sanitize(ulasanPengguna, {
  ALLOWED_TAGS: ["b", "i", "em", "strong", "a"],
});
panelKomentar.innerHTML = bersih;

Kode di atas adalah ilustrasi pemakaian — pustakanya dipasang lewat npm dan berjalan di browser, jadi tidak kita eksekusi di terminal. Cara kerjanya sesuai dokumentasinya: sanitize() menerima string HTML apa pun dan mengembalikan string HTML yang hanya memuat tag serta atribut pada daftar aman; atribut event seperti onerror dihapus, tag yang tidak dikenal dibuang, dan pola-pola jebakan parsing ditangani pustaka. Daftar ALLOWED_TAGS menegakkan prinsip yang sama dengan daftar izin protokol yang kita pakai nanti: yang tidak diketahui aman, ditolak.

Satu catatan praktis: DOMPurify bekerja di atas DOM — di browser ia memakai DOM bawaan, dan di lingkungan uji seperti Node ia dipasangkan dengan jsdom. Kabar baiknya pemakaian di halaman tidak butuh konfigurasi: pasang lewat npm (atau muat berkasnya), panggil sanitize(), sisanya pustaka yang menangani.

Kebiasaan ini juga merampingkan review kode: tempat satu-satunya yang boleh memanggil innerHTML adalah baris yang mampu menjelaskan dari mana stringnya berasal. Bila pertanyaan itu sulit dijawab, jawabannya hampir pasti textContent.

Satu kebiasaan yang sama pentingnya dengan menampilkan data adalah memutuskan kode mana yang boleh berjalan — dan string dari pihak lain bukanlah program. Itu urusan chapter berikutnya.

Berkas Sumber

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

escape-html.js

Unduh berkas
javascript
// escape-html.js — sanitasi dasar: teks pihak lain aman disisipkan ke markup
function escapeHtml(teks) {
  const peta = {
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#39;",
  };
  return teks.replace(/[&<>"']/g, (karakter) => peta[karakter]);
}

// 1. ulasan biasa yang memuat karakter khusus
console.log(escapeHtml("Karya & pena <terbaik> \"Budi\""));

// 2. input berisi markup utuh: seluruh markup ikut menjadi teks
console.log(escapeHtml('<img src=x onerror="alert(\'Punten\')">'));

// 3. hasil escape aman dibungkus markup milik kita sendiri
const kartu = `<div class="ulasan">${escapeHtml("Diskon <b>50%</b> & gratis ongkir")}</div>`;
console.log(kartu);

// 4. pemeriksaan: tidak ada sudut kurung mentah yang tersisa
const hasil = escapeHtml("a < b & c > d");
console.log(hasil);
console.log(/[<>]/.test(hasil), hasil.includes("&lt;"), hasil.includes("&amp;"));