Ulasan buku adalah pengisi yang menyenangkan untuk halaman toko Budi — sampai seorang pengunjung iseng mengetik potongan HTML ke dalam kolom ulasan. String itu masuk lewat form seperti data biasa, dan di situlah keputusan penting muncul: bagaimana halaman menampilkannya kembali? Salah satu jalur mengubah teks pengguna menjadi bagian halaman yang bisa menjalankan kode; jalur lainnya tetap memperlakukannya sebagai teks.

Chapter penutup section ini merangkum perlakuan aman atas input: menerima setiap isian sebagai data, bukan kode; menampilkannya dengan textContent alih-alih innerHTML; membersihkan spasi dan huruf sebelum dipakai; serta menegakkan batas panjang secara aktif. Topik keamanan web yang lebih dalam — serangan XSS, kebijakan Content Security — menanti pada section Security; pekerjaan kita di sini adalah fondasi kebiasaan yang benar.

Input adalah Data, Bukan Kode #

Prinsipnya satu kalimat: string yang diketik pengguna adalah teks, dan hanya kode yang kita tulis yang boleh dijalankan. Bila string pengguna disisipkan ke halaman sebagai markup, browser berhak memperlakukannya seperti markup lain — dan isi string itu bisa berupa elemen dengan penangan kejadian (event handler) bawaan. Satu teks, dua jalur penampilan: hasil keduanya kita buktikan dengan menghitung elemen yang lahir.

textContent, Bukan innerHTML #

Halaman ulasan menampung teks pengunjung di dalam wadah #daftar-ulasan. Dua jalur menulis ke wadah yang sama — dan perbedaannya bisa dibuktikan dengan menghitung elemen yang lahir:

// aman-kan-input.js — input adalah data, bukan kode
const box = document.querySelector("#daftar-ulasan");
const teksPengguna = "<img src=x onerror=alert(1)> Ulasan palsu.";

// cara keliru: input dimasukkan sebagai markup
box.innerHTML = teksPengguna;
console.log("innerHTML  : elemen anak =", box.children.length);

box.textContent = ""; // bersihkan percobaan pertama

// cara aman: input dimasukkan sebagai teks
box.textContent = teksPengguna;
console.log("textContent: elemen anak =", box.children.length);
console.log("tersimpan  :", box.innerHTML);

Output yang Dihasilkan (console browser):

text
innerHTML  : elemen anak = 1
textContent: elemen anak = 0
tersimpan  : &lt;img src=x onerror=alert(1)&gt; Ulasan palsu.

Mari kita baca pembuktiannya:

  1. Baris 3 (teksPengguna): satu string dari “pengunjung” berisi tag <img> dengan penangan onerror. Sebagai data, string ini tak berbahaya — bahaya muncul dari cara halaman memperlakukannya.
  2. Baris 6–7 (jalur innerHTML): innerHTML menguraikan string sebagai markup, dan keluaran elemen anak = 1 membuktikan bahwa browser benar-benar membuat elemen <img> dari teks pengguna. Pada halaman sungguhan, kegagalan memuat gambar itu memicu onerror — kode pengguna berjalan di halaman kita. Inilah pola dasar serangan XSS.
  3. Baris 12–14 (jalur textContent): textContent menyimpan string apa adanya sebagai teks. Keluaran elemen anak = 0 dan isi innerHTML yang ter-escape (&lt;img ...&gt;) menunjukkan browser menyimpan dan menampilkan teks tersebut tanpa pernah menguraikannya sebagai markup.

Normalisasi: Trim, Spasi, dan Huruf #

Data yang sah pun sering tiba dengan bentuk yang tidak rapi: spasi tersembunyi di awal dan akhir, jarak ganda di tengah, atau alamat email dengan huruf kapital yang tidak konsisten. Perlakuan aman juga berarti menyeragamkan data sebelum dipakai — dan String Methods serta regex yang sudah kita kuasai menyediakan semua alatnya. Tanpa penyeragaman, dua isian yang maknanya sama (“Budi” dan " Budi “) diperlakukan sebagai dua orang berbeda.

// normalisasi-input.js — bersihkan input sebelum dipakai
const form = document.querySelector("#form-ulasan");
const nama = form.querySelector("#nama-ulasan");
const ulasan = form.querySelector("#teks-ulasan");
const hitung = form.querySelector("#hitung-ulasan");

const BATAS = 60;

// penghitung karakter merespons setiap ketikan
ulasan.addEventListener("input", function () {
  hitung.textContent = ulasan.value.length + " / " + BATAS + " karakter";
});

form.addEventListener("submit", function (event) {
  event.preventDefault();

  const namaBersih = nama.value.trim().replace(/\s+/g, " ");
  const teksBersih = ulasan.value.trim().replace(/\s+/g, " ").slice(0, BATAS);
  const emailBersih = "  [email protected] ".trim().toLowerCase();

  console.log("nama   :", JSON.stringify(namaBersih));
  console.log("teks   :", JSON.stringify(teksBersih));
  console.log("panjang:", teksBersih.length, "dari", ulasan.value.length);
  console.log("email  :", emailBersih);
  console.log("hitung :", hitung.textContent);
});

Simulasi: nama diisi " Siti Rahma " dengan spasi liar, textarea diisi ulasan sepanjang 100 karakter yang membawa beberapa spasi ganda, lalu form dikirim.

Output yang Dihasilkan (console browser):

text
nama   : "Siti Rahma"
teks   : "Kisah sejarah Bandung dibahas rapi, bahasanya mudah diikuti."
panjang: 60 dari 100
email  : [email protected]
hitung : 100 / 60 karakter

Yang bekerja di balik keluaran itu:

  1. Baris 17 (namaBersih): trim() memangkas spasi di kedua ujung, dan replace(/\s+/g, " ") meringkas setiap rangkaian spasi menjadi satu. Hasilnya satu bentuk kanonik untuk semua isian yang setara — membandingkan nama, mencari duplikat, atau mengurutkan kini konsisten.
  2. Baris 18 (teksBersih): rantai yang sama ditutup slice(0, BATAS) — batas panjang yang ditegakkan oleh kode kita sendiri. Keluaran panjang: 60 dari 100 memperlihatkan hasil potongannya: 100 karakter mentah menjadi tepat 60 karakter tersimpan, sisanya dihilangkan secara sadar.
  3. Baris 19 (emailBersih): trim() lalu toLowerCase() membuat perbandingan email tidak peduli kapitalisasi. Penyimpanan yang menyeragamkan sejak awal mencegah satu orang memiliki dua akun karena perbedaan huruf besar-kecil.
  4. Baris 10–12 (penghitung): listener input memperbarui hitung.textContent pada setiap ketikan — dan baris hitung : 100 / 60 karakter pada keluaran menunjukkan angka yang belum dipotong, karena penghitung membaca .value mentah. Hanya data yang bersih di baris 18 yang boleh dipakai lebih lanjut.

Perhatikan pula urutan pengerjaan di baris 17–19: bersihkan dulu, potong kemudian. Bila potongan dilakukan sebelum normalisasi, spasi ganda bisa menggeser posisi karakter dan jumlah akhirnya tidak pasti. Normalisasi di awal membuat setiap langkah berikutnya bekerja pada bentuk yang sudah stabil.

Batas Panjang yang Tidak Boleh Diletakkan pada Browser #

Form kita membawa maxlength="60", dan memang browser memotong ketikan pengguna pada batas itu. Tetapi atribut ini bukan tembok: nilai yang ditetapkan lewat kode — atau ditempelkan lewat jalur tertentu — tidak selalu melewati pemangkasan maxlength, dan lingkungan uji jsdom memang menunjukkan perilaku itu (nilai 100 karakter tetap utuh). Karena itu batas ditegakkan dua kali: maxlength untuk kenyamanan mengetik, slice() di kode untuk kepastian data. Server tetap melakukan pemeriksaan terakhirnya sendiri.

Ringkasan Perlakuan Aman #

Sebagai penutup section, lima kebiasaan ini layak dibawa ke setiap form berikutnya:

SituasiPilihan yang aman
Menampilkan teks penggunatextContent — jangan pernah innerHTML dengan isi pengguna
Membandingkan isiannormalisasi dulu: trim(), rapikan spasi, toLowerCase() untuk email
Membatasi panjangmaxlength untuk kenyamanan, slice() untuk kepastian
Menyusun markup dinamiscreateElement + textContent per simpul
Kepercayaan terakhirvalidasi ulang di server — kode sisi klien selalu terbuka

Section ini menutup perjalanan form: membaca data, memvalidasi, mengirim tanpa berpindah halaman, mengendalikan perilaku bawaan, memberi umpan balik yang bisa didengar semua orang, dan memperlakukan isian dengan aman. Section berikutnya membuka pintu ke kemampuan browser yang lebih luas — dimulai dari fetch untuk berbicara dengan server sungguhan.

Berkas Sumber

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

aman-kan-input.js

Unduh berkas
javascript
// aman-kan-input.js — input adalah data, bukan kode
const box = document.querySelector("#daftar-ulasan");
const teksPengguna = "<img src=x onerror=alert(1)> Ulasan palsu.";

// cara keliru: input dimasukkan sebagai markup
box.innerHTML = teksPengguna;
console.log("innerHTML  : elemen anak =", box.children.length);

box.textContent = ""; // bersihkan percobaan pertama

// cara aman: input dimasukkan sebagai teks
box.textContent = teksPengguna;
console.log("textContent: elemen anak =", box.children.length);
console.log("tersimpan  :", box.innerHTML);

normalisasi-input.js

Unduh berkas
javascript
// normalisasi-input.js — bersihkan input sebelum dipakai
const form = document.querySelector("#form-ulasan");
const nama = form.querySelector("#nama-ulasan");
const ulasan = form.querySelector("#teks-ulasan");
const hitung = form.querySelector("#hitung-ulasan");

const BATAS = 60;

// penghitung karakter merespons setiap ketikan
ulasan.addEventListener("input", function () {
  hitung.textContent = ulasan.value.length + " / " + BATAS + " karakter";
});

form.addEventListener("submit", function (event) {
  event.preventDefault();

  const namaBersih = nama.value.trim().replace(/\s+/g, " ");
  const teksBersih = ulasan.value.trim().replace(/\s+/g, " ").slice(0, BATAS);
  const emailBersih = "  [email protected] ".trim().toLowerCase();

  console.log("nama   :", JSON.stringify(namaBersih));
  console.log("teks   :", JSON.stringify(teksBersih));
  console.log("panjang:", teksBersih.length, "dari", ulasan.value.length);
  console.log("email  :", emailBersih);
  console.log("hitung :", hitung.textContent);
});