Mengapa Satu Cara Escaping Saja Tidak Cukup untuk Semua Konteks?
Cross-Site Scripting (XSS) terjadi ketika data dinamis yang berasal dari luar (input pengguna, database, atau API pihak ketiga) dicetak ke dalam halaman web sedemikian rupa sehingga peramban menafsirkannya sebagai instruksi kode yang harus dijalankan, bukan sebagai teks data biasa.
Kunci utama pencegahan XSS menurut OWASP Cross Site Scripting Prevention Cheat Sheet adalah Contextual Output Encoding: cara Anda mengodekan keluaran bergantung pada di bagian mana dari dokumen variabel tersebut disisipkan!
Di Mana Variabel Dicetak? Fungsi Pengaman yang Wajib Dipakai di PHP
───────────────────────────────────────────── ──────────────────────────────────────────────────────
1. Di dalam Isi Elemen HTML (<p>INI</p>) ──> htmlspecialchars($val, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
2. Di dalam Atribut Berkutip ("INI") ──> htmlspecialchars($val, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
3. Di dalam Nilai Variabel JS (<script>) ──> json_encode($val, JSON_HEX_TAG | JSON_HEX_AMP | ...)
4. Di dalam Parameter Query URL (?q=INI) ──> rawurlencode($val) atau http_build_query([...])
5. Di dalam Atribut URL (href="INI") ──> Validasi skema protokol ('https'/'http') + htmlspecialchars()
1. Konteks 1 & 2: Teks Elemen HTML dan Atribut HTML (htmlspecialchars)
Buat satu fungsi pembantu (helper) pendek e() yang selalu siap dipanggil di seluruh templat tampilan Anda:
<?php
declare(strict_types=1);
function e(string|int|float|null $nilai): string
{
return htmlspecialchars(
string: (string) $nilai,
flags: ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5,
encoding: 'UTF-8',
);
}
Mengapa Ketiga Argumen htmlspecialchars() Tersebut Penting?
ENT_QUOTES: Memastikan bahwa baik tanda kutip ganda ("→") maupun tanda kutip tunggal ('→'pada ENT_HTML5 /'pada HTML 4.01) sama-sama diubah menjadi entitas aman!ENT_SUBSTITUTE: Jika di dalam string terdapat urutan byte UTF-8 yang rusak/tidak valid, daripada fungsi gagal dan mengembalikan string kosong""secara tiba-tiba,ENT_SUBSTITUTEmengganti byte rusak tersebut dengan simbol karakter pengganti UnicodeU+FFFD(�) yang aman!'UTF-8': Menjamin fungsi mengurai batas karakter multibyte sesuai pengodean UTF-8 dokumen Anda.
htmlspecialchars()- Atribut Tanpa Tanda Kutip (
<input value=<?= e($x) ?>>): Selalu bungkus nilai atribut dengan tanda kutip ganda (value="<?= e($x) ?>"), karenahtmlspecialchars()tidak mengubah karakter spasi! - Atribut Event Handler (
onclick="...",onload="...",onerror="..."): Jangan pernah menyisipkan data pengguna ke dalam atributon*! Gunakan atributdata-*(data-nama="<?= e($nama) ?>") lalu baca dari JavaScript menggunakanelemen.dataset.nama. - Nama Tag atau Nama Atribut (
<<?= e($tag) ?>>): Jika nama tag harus dinamis (misalnya memilih antarah2atauh3), wajib saring dengan daftar putihmatch ($level) { 2 => 'h2', default => 'h3' }.
2. Konteks 3: Menyisipkan Data PHP ke Dalam JavaScript (json_encode)
Jika Anda perlu mengirimkan objek konfigurasi atau data awal dari PHP ke variabel JavaScript di dalam <script>, jangan menggunakan htmlspecialchars() (karena di dalam blok <script>, peramban tidak menerjemahkan " kembali menjadi tanda kutip!).
Sebaliknya, gunakan json_encode() dengan empat bendera JSON_HEX_* atau letakkan datanya di dalam atribut data-* pada elemen HTML:
<?php
declare(strict_types=1);
function jsonUntukScript(mixed $data): string
{
return json_encode(
value: $data,
flags: JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR,
);
}
$profilAwal = [
'nama' => 'Budi </script><script>alert(1)</script>',
'peran' => 'peserta',
];
?>
<!-- Cara A: Menggunakan jsonUntukScript() dengan JSON_HEX_TAG -->
<script>
const PROFIL = <?= jsonUntukScript($profilAwal) ?>;
console.log(PROFIL.nama);
</script>
<!-- Cara B (Lebih Bersih & Ramah CSP Strict!): Simpan di atribut data-* lalu baca dengan JSON.parse() -->
<div id="data-awal" data-profil="<?= e(json_encode($profilAwal, JSON_THROW_ON_ERROR)) ?>" hidden></div>
<script src="/assets/app.js" defer></script>
Perhatikan Cara B di atas: dengan menaruh JSON di dalam atribut data-profil="..." yang di-escape dengan e(), halaman Anda tidak membutuhkan blok <script> inline sama sekali, sehingga Anda dapat mengaktifkan kebijakan Content-Security-Policy: script-src 'self' yang paling ketat!
3. Konteks 4 & 5: Membangun Tautan URL yang Aman (http_build_query & Validasi Skema)
- Saat menyisipkan parameter ke dalam query URL: Gunakan
http_build_query()ataurawurlencode(), lalu bungkus keseluruhan URL dengane()saat dimasukkan ke dalam atributhref="...": ``php <?php $urlPencarian = '/cari?' . http_build_query([ 'q' => 'C++ & PHP 8.5', 'urut' => 'terbaru', ]); ?> <a href="<?= e($urlPencarian) ?>">Cari Materi</a>`` - Saat seluruh URL berasal dari input pengguna (misalnya tautan situs web profil):
htmlspecialchars("javascript:alert(1)")tetap menghasilkan"javascript:alert(1)"karena tidak mengandung karakter<,>, atau"! Oleh karena itu, wajib periksa bahwa skema URL hanya boleh'https'atau'http'terlebih dahulu: ```php <?php
declare(strict_types=1);
function bersihkanUrlProfil(string $urlInput): ?string { $url = trim($urlInput); if (!filter_var($url, FILTER_VALIDATE_URL)) { return null; }
$skema = strtolower((string) parse_url($url, PHP_URL_SCHEME)); if ($skema !== 'https' && $skema !== 'http') { return null; // Menolak 'javascript:', 'data:', 'vbscript:', dll. }
return $url; } ```
4. Memperkuat Respons dengan Header Keamanan HTTP (header())
Sebagai pertahanan berlapis di setiap respons halaman HTML, kirimkan header keamanan HTTP berikut sebelum mencetak output:
<?php
declare(strict_types=1);
function kirimHeaderKeamananHtml(): void
{
header('Content-Type: text/html; charset=utf-8');
// Mencegah peramban menebak-nebak (MIME-sniffing) berkas teks menjadi skrip yang bisa dijalankan
header('X-Content-Type-Options: nosniff');
// Mencegah halaman disematkan di dalam <iframe> situs lain (Anti-Clickjacking)
header('X-Frame-Options: DENY');
// Mengendalikan informasi Referer saat pengguna mengeklik tautan keluar
header('Referrer-Policy: strict-origin-when-cross-origin');
// Membatasi sumber skrip hanya dari domain sendiri (Anti-XSS berlapis!)
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https:; frame-ancestors 'none'; base-uri 'self'");
}
Latihan Praktik: Membuat Kelas Pembantu Template yang Aman Kontekstual
Buatlah berkas TemplateAman.php yang menyediakan tiga metode statis:
TemplateAman::teks(string|int|float|null $val): string(menggunakanhtmlspecialchars).TemplateAman::json(mixed $data): string(menggunakanjson_encodedenganJSON_HEX_*).TemplateAman::url(string $url): string(memvalidasi protokolhttp/httpsdan mengembalikan'#'jika berbahaya, lalu di-escape denganteks()).- Uji ketiga metode tersebut dengan muatan uji
<script>alert(1)</script>danjavascript:alert(1).