XSS (cross-site scripting) adalah serangan yang menyisipkan kode HTML atau JavaScript melalui masukan pengguna sehingga browser menjalankannya seolah-olah berasal dari aplikasi sendiri. Topik ini membahas cara kerja serangan tersebut, pencegahannya menggunakan htmlspecialchars, dan disiplin memilih pelolosan sesuai konteks keluaran. Setelah membacanya, setiap titik cetak data dinamis pada halaman dapat diperiksa dan diamankan secara sistematis.
Cara kerja serangan XSS#
Serangan XSS bermula dari satu penyebab, yaitu server yang menggabungkan data dari luar langsung ke dalam HTML tanpa pelolosan. Browser tidak dapat membedakan markah yang memang ditulis aplikasi dari teks yang berasal dari masukan pengguna, sehingga keduanya diproses dengan aturan yang sama. Apabila sebuah aplikasi mencetak komentar pengguna apa adanya, komentar berisi <script>alert("XSS")</script> akan dieksekusi oleh browser setiap pengunjung yang membuka halaman itu.
Dampaknya tidak berhenti pada pesan yang mengganggu. Skrip sisipan dapat membaca isi halaman, mengirim cookie ke server milik penyerang, atau menjalankan aksi atas nama pengguna yang sedang login. Halaman admin pun menjadi sasaran karena serangan cukup ditanam pada data yang kelak dibaca halaman tersebut.
Bentuk pencegahan utamanya adalah pelolosan (escaping), yaitu mengganti karakter khusus HTML dengan entitasnya sehingga teks tidak lagi terbaca sebagai markah. Fungsi standarnya adalah htmlspecialchars, dan pemakaiannya dibahas pada bagian berikut.
Meloloskan keluaran dengan htmlspecialchars#
Fungsi htmlspecialchars mengganti lima karakter yang berperan dalam struktur HTML menjadi entitas, dan tabel berikut merangkum konversinya.
| Karakter | Menjadi | Peran dalam HTML |
|---|---|---|
< | < | pembuka tag |
> | > | penutup tag |
& | & | pembuka entitas |
" | " | pembatas nilai atribut |
' | ' | pembatas nilai atribut |
Berkas berikut menerapkan fungsi tersebut pada masukan yang memuat tag <script>.
<?php
$masukan = '<script>alert("XSS")</script>';
$aman = htmlspecialchars($masukan, ENT_QUOTES, 'UTF-8');
echo 'Masukan : ' . $masukan . PHP_EOL;
echo 'Setelah pelolosan : ' . $aman . PHP_EOL;
echo 'Panjang sebelum : ' . strlen($masukan) . PHP_EOL;
echo 'Panjang sesudah : ' . strlen($aman) . PHP_EOL;Masukan : <script>alert("XSS")</script>
Setelah pelolosan : <script>alert("XSS")</script>
Panjang sebelum : 29
Panjang sesudah : 51Keluaran memperlihatkan dua hal. Teks hasil pelolosan masih memuat rangkaian karakter yang sama, tetapi seluruh tanda struktural telah menjadi entitas. Browser karenanya menampilkan teks itu apa adanya dan tidak membangun elemen apa pun darinya. Panjang string bertambah dari 29 menjadi 51 karakter karena setiap karakter khusus diganti rangkaian entitas yang lebih panjang.
Panggilan pada contoh menyertakan dua argumen tambahan. Konstanta ENT_QUOTES memerintahkan pelolosan kedua jenis tanda kutip, sedangkan 'UTF-8' menyatakan pengodean masukan agar konversi benar untuk karakter selain ASCII. Kedua argumen itu sebaiknya selalu ditulis karena nilai bawaan fungsi ini pernah berubah antar versi PHP, sehingga penulisan eksplisit menjaga perilaku yang sama di semua lingkungan.
Menyesuaikan pelolosan dengan konteks keluaran#
Data dinamis dapat berlabuh pada beberapa tempat di dalam halaman, dan setiap tempat menuntut pelolosan yang berbeda. Konteks yang paling sering dipakai adalah badan HTML, nilai atribut, dan kode JavaScript, sedangkan bagian URL dari sebuah tautan menuntut penanganan tersendiri melalui urlencode.
| Konteks | Contoh tempatnya | Pelolosan yang tepat |
|---|---|---|
| Badan HTML | antara <p> dan </p> | htmlspecialchars |
| Nilai atribut | value="..." | htmlspecialchars beserta ENT_QUOTES |
| Kode JavaScript | di dalam <script> | json_encode |
| Bagian URL | href="cari.php?q=..." | urlencode |
Nilai atribut menuntut ENT_QUOTES karena pembatasnya adalah tanda kutip. Tanpa pelolosan kutip, masukan yang memuat tanda kutip ganda dapat keluar dari atribut dan menyuntikkan atribut baru milik penyerang. Contoh berikut membandingkan keduanya.
<?php
$kata = 'Sejarah "<Nusantara>"';
echo '<input value="' . $kata . '">' . PHP_EOL;
echo '<input value="' . htmlspecialchars($kata, ENT_QUOTES, 'UTF-8') . '">' . PHP_EOL;<input value="Sejarah "<Nusantara>"">
<input value="Sejarah "<Nusantara>"">Baris pertama rusak karena kutip pada masukan menutup atribut value lebih awal, sedangkan baris kedua tetap utuh karena seluruh kutip telah menjadi entitas. Kode JavaScript menuntut perlakuan yang berbeda lagi. Teks </script> di dalam string JavaScript tetap mengakhiri elemen <script> menurut parser HTML, sehingga pelolosan HTML saja tidak cukup.
<?php
$kata = '</script><b>tes</b>';
echo 'Tanpa json_encode : var kata = "' . $kata . '";' . PHP_EOL;
echo 'Dengan json_encode: var kata = ' . json_encode($kata) . ';' . PHP_EOL;Tanpa json_encode : var kata = "</script><b>tes</b>";
Dengan json_encode: var kata = "<\/script><b>tes<\/b>";Fungsi json_encode menghasilkan literal JavaScript yang aman karena tanda kutip dan garis miringnya diloloskan, sehingga string tersebut tidak lagi mengakhiri elemen <script> secara prematur. Cara mencetak mentah dan cara mencetak sebagai teks murni dapat dicoba pada demo berikut. Ketik markah seperti <b>halo</b>, tekan tombol Cetak mentah, kemudian tekan tombol Cetak aman, dan amati perbedaan keduanya.
Jalur mentah membangun elemen <b> sungguhan sehingga teksnya tampil tebal, sedangkan jalur aman menampilkan karakternya apa adanya. Aturan yang sama berlaku pada skrip berbahaya, yaitu jalur mentah mengeksekusinya dan jalur aman menampilkannya sebagai teks biasa.
Kesalahan umum#
Empat kesalahan berikut paling sering muncul saat mencegah XSS.
- Data dinamis dicetak tanpa
htmlspecialchars. Tag dari masukan ikut dirender dan dapat dieksekusi browser. Perbaikannya adalah membungkus setiap data dinamis denganhtmlspecialcharsbesertaENT_QUOTESdan'UTF-8'di setiap titik cetak. - Pelolosan dilakukan sekali saat data disimpan. Data yang tersimpan berbentuk entitas sulit dipakai ulang untuk pencarian atau keluaran non-HTML, dan pelolosan ganda menampilkan entitas secara harfiah. Perbaikannya adalah menyimpan data mentah lalu meloloskan saat mencetak.
- Fungsi
strip_tagsdianggap pengganti pelolosan. Fungsi itu membuang tag tetapi tidak mengubah tanda kutip, sehingga injeksi atribut tetap mungkin. Perbaikannya adalah tetap menggunakanhtmlspecialcharsuntuk konteks HTML. - Konteks JavaScript diisi pelolosan HTML. Rangkaian
<tidak menghentikan parser HTML membaca penutup elemen<script>. Perbaikannya adalahjson_encodeuntuk data yang masuk ke kode JavaScript.
Ringkasan#
- XSS terjadi karena data dari luar digabungkan langsung ke dalam HTML sehingga browser memprosesnya sebagai markah.
htmlspecialcharsmengganti lima karakter khusus HTML menjadi entitas, danENT_QUOTESbeserta'UTF-8'disertakan agar tanda kutip ikut terloloskan.- Konteks keluaran menentukan pelolosan yang tepat, yaitu
htmlspecialcharsuntuk badan HTML dan atribut,json_encodeuntuk JavaScript, sertaurlencodeuntuk bagian URL. - Pelolosan dilakukan pada saat mencetak, sedangkan data disimpan dalam bentuk mentahnya.
Referensi#
- htmlspecialchars — PHP Manual — Dokumentasi pelolosan karakter khusus HTML beserta bendera yang tersedia.
- json_encode — PHP Manual — Dokumentasi pengubahan nilai PHP menjadi literal JSON untuk konteks JavaScript.
- urlencode — PHP Manual — Dokumentasi pelolosan karakter khusus URL pada query string.
- Cross Site Scripting Prevention Cheat Sheet — OWASP — Panduan konteks keluaran beserta aturan pelolosan masing-masing.