Di episode #8 kita membahas SQL Injection — input user yang disuntikkan ke query database. Kali ini saya lanjut ke kerabat dekatnya yang justru lebih sering muncul di daftar temuan: Cross-Site Scripting (XSS). Bedanya, kalau SQLi menyasar server dan database, XSS menyasar browser milik orang lain. Di OWASP Top 10:2025, XSS masih berada di dalam kategori A05 Injection, dan dari catatan OWASP Injection adalah satu-satunya kategori yang diuji pada 100% aplikasi dalam dataset mereka, dengan total 62.445 CVE — di mana XSS (CWE-79) menyumbang lebih dari 30.000 di antaranya.

Kenapa browser juga perlu “memfilter” input
Sama seperti database, browser tidak bisa membedakan mana teks yang ditulis developer dan mana yang diketik user. Kalau user mengetik nama di kolom komentar, dan server menyimpannya apa adanya lalu mengembalikannya ke halaman, browser akan menerima teks itu sebagai HTML. Begitu ada tag <script> di dalamnya, browser akan mengeksekusi — bukan sekadar menampilkan. Korbannya bukan server, tapi sesi login, cookie, dan identitas pengunjung halaman tersebut.
Tiga wujud XSS yang saya catat
Yang bikin saya awalnya bingung: ketiganya memakai payload serupa, tapi tempat payload itu “bersembunyi” berbeda. Menurut PortSwigger, ketiganya dibedakan dari bagaimana data jahat itu sampai ke browser.
| Jenis | Bagaimana payload sampai | Dampak |
|---|---|---|
| Reflected (pantulan) | Payload ada di URL atau form, langsung dipantulkan server ke halaman respons tanpa disimpan | Korban harus membuka link yang sudah dimanipulasi penyerang |
| Stored (tersimpan) | Payload disimpan di database — kolom komentar, bio profil, subject email | Paling berbahaya: semua orang yang melihat data itu ikut terpapar |
| DOM-based | Server bersih, tapi JavaScript membaca data dari URL lalu menulisnya ke DOM dengan cara tidak aman | Sepenuhnya terjadi di sisi klien, tidak terlihat di log server |
Contoh input yang tidak boleh saya kembalikan mentah
Berikut pola paling sederhana yang pernah saya temukan di kode lama saya sendiri — nama user dicetak langsung ke halaman tanpa sanitasi:
<?php // Berbahaya: input user langsung jadi bagian HTML echo "<p>Halo, " . $_GET['nama'] . "</p>";
Kalau URL dibuka dengan ?nama=<script>alert(1)</script>, browser akan menampilkan pesan itu sebagai dialog, bukan teks. Dalam kasus nyata, payload seperti ini biasanya bukan alert(1), tapi skrip pencuri cookie atau keylogger halaman.
Obat utama: escape sesuai konteks, bukan sekadar hapus tag
Kesalahan yang pernah saya lakukan adalah mengandalkan strip_tags() untuk “membersihkan” input. Masalahnya, konteks penempatan berbeda: teks di dalam <p> butuh escape HTML, teks di dalam atribut butuh perlakuan lain, dan teks yang masuk ke dalam <script> butuh escape JavaScript. PHP sudah menyediakan htmlspecialchars dengan mode ENT_QUOTES dan charset eksplisit untuk kasus HTML — dan itu yang saya pakai sekarang untuk output ke template.
<?php // Aman: karakter HTML di-encode sebelum dicetak $nama = htmlspecialchars($_GET['nama'], ENT_QUOTES, 'UTF-8'); echo "<p>Halo, " . $nama . "</p>";
Content Security Policy: jaring pengaman kedua
Menurut dokumentasi MDN, header HTTP Content-Security-Policy mengontrol resource mana yang boleh dimuat halaman — dan ini adalah pertahanan terakhir kalau ada satu baris escaping yang terlewat. Directive yang paling saya andalkan: script-src untuk membatasi asal skrip, object-src 'none' untuk mematikan plugin lama, frame-ancestors untuk melindungi dari clickjacking, serta base-uri dan form-action. Praktik yang saya ikuti dari panduan implementasi MDN: gunakan nonce acak per permintaan untuk skrip inline, bukan memasukkan 'unsafe-inline' ke allowlist — karena begitu unsafe-inline aktif, CSP praktis kehilangan perlindungan XSS-nya. Spesifikasi CSP Level 3 sendiri masih dalam tahap Working Draft W3C (diperbarui 16 September 2026), jadi direktif seperti report-to dan Reporting API untuk memantau pelanggaran kebijakan masih terus berevolusi.
Yang saya terapkan minggu ini
Saya audit semua titik output di project: template blade, komentar user, dan kolom bio profil. Escape sesuai konteks, aktifkan htmlspecialchars di helper yang saya pakai, lalu coba pasang CSP header secara report-only dulu supaya tidak langsung memblokir resource yang belum terdaftar. Di episode #7 kita sudah bicara attack surface; XSS mengingatkan saya bahwa satu baris echo yang lupa escape bisa membuka jalan ke seluruh sesi pengguna. Episode berikutnya kita lanjut ke CSRF dan Clickjacking — dua serangan yang tidak butuh injeksi kode, tapi cukup memanfaatkan kepercayaan browser terhadap situs yang sedang dikunjungi korban.
Sumber
- OWASP Top 10:2025 — A05 Injection (XSS > 30k CVE, CWE-79) — top10.owasp.org
- PortSwigger Web Security Academy — Cross-site scripting (reflected, stored, DOM-based) — portswigger.net
- MDN Web Docs — Content-Security-Policy header — developer.mozilla.org
- MDN — Practical implementation guide: CSP (nonce vs unsafe-inline) — developer.mozilla.org
- W3C — Content Security Policy Level 3, Working Draft 16 September 2026 — w3.org
- OWASP XSS Prevention Cheat Sheet — cheatsheetseries.owasp.org
Komentar Terbaru