Pencegahan XSS

htmlspecialchars dan disiplin konteks keluaran.

Daftar isi

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.

KarakterMenjadiPeran dalam HTML
<&lt;pembuka tag
>&gt;penutup tag
&&amp;pembuka entitas
"&quot;pembatas nilai atribut
'&#039;pembatas nilai atribut

Berkas berikut menerapkan fungsi tersebut pada masukan yang memuat tag <script>.

php pelolosan.php
<?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;
text Keluaran
Masukan           : <script>alert("XSS")</script>
Setelah pelolosan : &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;
Panjang sebelum   : 29
Panjang sesudah   : 51

Keluaran 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.

KonteksContoh tempatnyaPelolosan yang tepat
Badan HTMLantara <p> dan </p>htmlspecialchars
Nilai atributvalue="..."htmlspecialchars beserta ENT_QUOTES
Kode JavaScriptdi dalam <script>json_encode
Bagian URLhref="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
<?php
$kata = 'Sejarah "<Nusantara>"';

echo '<input value="' . $kata . '">' . PHP_EOL;
echo '<input value="' . htmlspecialchars($kata, ENT_QUOTES, 'UTF-8') . '">' . PHP_EOL;
text Keluaran
<input value="Sejarah "<Nusantara>"">
<input value="Sejarah &quot;&lt;Nusantara&gt;&quot;">

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
<?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;
text Keluaran
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 dengan htmlspecialchars beserta ENT_QUOTES dan '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_tags dianggap pengganti pelolosan. Fungsi itu membuang tag tetapi tidak mengubah tanda kutip, sehingga injeksi atribut tetap mungkin. Perbaikannya adalah tetap menggunakan htmlspecialchars untuk konteks HTML.
  • Konteks JavaScript diisi pelolosan HTML. Rangkaian &lt; tidak menghentikan parser HTML membaca penutup elemen <script>. Perbaikannya adalah json_encode untuk data yang masuk ke kode JavaScript.

Ringkasan#

  • XSS terjadi karena data dari luar digabungkan langsung ke dalam HTML sehingga browser memprosesnya sebagai markah.
  • htmlspecialchars mengganti lima karakter khusus HTML menjadi entitas, dan ENT_QUOTES beserta 'UTF-8' disertakan agar tanda kutip ikut terloloskan.
  • Konteks keluaran menentukan pelolosan yang tepat, yaitu htmlspecialchars untuk badan HTML dan atribut, json_encode untuk JavaScript, serta urlencode untuk bagian URL.
  • Pelolosan dilakukan pada saat mencetak, sedangkan data disimpan dalam bentuk mentahnya.

Referensi#