Belajar Cyber Security #9: XSS — Reflected, Stored, DOM, dan Content Security Policy

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.

Diagram tiga jenis Cross-Site Scripting: Reflected, Stored, DOM-based
Tiga jalur XSS — payload serupa, tapi tempat penyimpanannya berbeda, dan itu menentukan cara membersihkannya.

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.

JenisBagaimana payload sampaiDampak
Reflected (pantulan)Payload ada di URL atau form, langsung dipantulkan server ke halaman respons tanpa disimpanKorban harus membuka link yang sudah dimanipulasi penyerang
Stored (tersimpan)Payload disimpan di database — kolom komentar, bio profil, subject emailPaling berbahaya: semua orang yang melihat data itu ikut terpapar
DOM-basedServer bersih, tapi JavaScript membaca data dari URL lalu menulisnya ke DOM dengan cara tidak amanSepenuhnya 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

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *

Situs ini menggunakan Akismet untuk mengurangi spam. Pelajari bagaimana data komentar Anda diproses