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):
innerHTML : elemen anak = 1
textContent: elemen anak = 0
tersimpan : <img src=x onerror=alert(1)> Ulasan palsu.Mari kita baca pembuktiannya:
- Baris 3 (
teksPengguna): satu string dari “pengunjung” berisi tag<img>dengan penanganonerror. Sebagai data, string ini tak berbahaya — bahaya muncul dari cara halaman memperlakukannya. - Baris 6–7 (jalur innerHTML):
innerHTMLmenguraikan string sebagai markup, dan keluaranelemen anak = 1membuktikan bahwa browser benar-benar membuat elemen<img>dari teks pengguna. Pada halaman sungguhan, kegagalan memuat gambar itu memicuonerror— kode pengguna berjalan di halaman kita. Inilah pola dasar serangan XSS. - Baris 12–14 (jalur textContent):
textContentmenyimpan string apa adanya sebagai teks. Keluaranelemen anak = 0dan isiinnerHTMLyang ter-escape (<img ...>) 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):
nama : "Siti Rahma"
teks : "Kisah sejarah Bandung dibahas rapi, bahasanya mudah diikuti."
panjang: 60 dari 100
email : [email protected]
hitung : 100 / 60 karakterYang bekerja di balik keluaran itu:
- Baris 17 (
namaBersih):trim()memangkas spasi di kedua ujung, danreplace(/\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. - Baris 18 (
teksBersih): rantai yang sama ditutupslice(0, BATAS)— batas panjang yang ditegakkan oleh kode kita sendiri. Keluaranpanjang: 60 dari 100memperlihatkan hasil potongannya: 100 karakter mentah menjadi tepat 60 karakter tersimpan, sisanya dihilangkan secara sadar. - Baris 19 (
emailBersih):trim()lalutoLowerCase()membuat perbandingan email tidak peduli kapitalisasi. Penyimpanan yang menyeragamkan sejak awal mencegah satu orang memiliki dua akun karena perbedaan huruf besar-kecil. - Baris 10–12 (penghitung): listener
inputmemperbaruihitung.textContentpada setiap ketikan — dan barishitung : 100 / 60 karakterpada keluaran menunjukkan angka yang belum dipotong, karena penghitung membaca.valuementah. 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:
| Situasi | Pilihan yang aman |
|---|---|
| Menampilkan teks pengguna | textContent — jangan pernah innerHTML dengan isi pengguna |
| Membandingkan isian | normalisasi dulu: trim(), rapikan spasi, toLowerCase() untuk email |
| Membatasi panjang | maxlength untuk kenyamanan, slice() untuk kepastian |
| Menyusun markup dinamis | createElement + textContent per simpul |
| Kepercayaan terakhir | validasi 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// 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// 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);
});