Kodeloka
  1. Kodeloka
  2. Kelas Keamanan Aplikasi
  3. Materi

2 · Injeksi & Input · Modul 3 dari 8

XSS & Keamanan Sisi Peramban

Tiga bentuk XSS, kenapa pengodean harus sesuai konteks, dan bagaimana CSP membatasi kerusakan.

Tiga Bentuk yang Perlu Dibedakan

Tersimpan (stored)   payload masuk basis data, menyerang setiap pengunjung
Terpantul (reflected) payload ada di URL, menyerang korban yang membuka tautan
Berbasis DOM         payload tidak pernah menyentuh server sama sekali

XSS berbasis DOM sering luput dari pengujian karena payload-nya berada setelah tanda pagar dan tidak pernah dikirim ke server — sehingga tidak muncul di log mana pun.

// Rentan — fragmen URL langsung ditulis ke halaman
document.getElementById("salam").innerHTML =
    "Halo, " + location.hash.slice(1);

// https://contoh.id/#<img src=x onerror=alert(1)>

Pengodean Bergantung pada Konteks

Tidak ada satu fungsi "escape" yang benar di segala tempat. Karakter yang berbahaya berbeda-beda menurut posisi penyisipan:

Isi elemen HTML      < > &             → &lt; &gt; &amp;
Nilai atribut        kutip, spasi        → selalu bungkus dengan kutip
Dalam JavaScript     kutip, backslash    → JSON.stringify, bukan escape HTML
Dalam URL            seluruh komponen    → encodeURIComponent
Dalam CSS            kurung, kutip       → hindari nilai dari pengguna
🚫 Escape HTML di dalam blok script tidak menolong sama sekali

Menyisipkan data ke dalam <script> dengan pengodean HTML tidak berpengaruh, karena isi elemen script tidak diurai sebagai HTML. Nilai &quot; tetap terbaca sebagai tanda kutip oleh mesin JavaScript, dan payload lolos. Gunakan JSON.stringify, atau lebih baik lagi taruh data pada atribut data- lalu baca dari JavaScript.

<!-- Rentan meski sudah "di-escape" -->
<script>const nama = "{{ nama|escape }}";</script>

<!-- Lebih aman -->
<div id="data" data-nama="{{ nama|escape }}"></div>
<script>
  const nama = document.getElementById("data").dataset.nama;
</script>

Kerangka Kerja Modern Aman — Sampai Dilangkahi

// Aman — React meng-escape otomatis
<div>{komentarPengguna}</div>

// Berbahaya — pintu keluar yang menonaktifkan proteksi
<div dangerouslySetInnerHTML={{ __html: komentarPengguna }} />

// Vue
<div v-html="komentarPengguna"></div>      // sama berbahayanya
<div>{{ komentarPengguna }}</div>           // aman

Bila HTML Memang Harus Diizinkan

import DOMPurify from "dompurify";

const bersih = DOMPurify.sanitize(masukan, {
    ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "ul", "ol", "li"],
    ALLOWED_ATTR: ["href", "title"],
    ALLOWED_URI_REGEXP: /^https?:\/\//i
});
⚠️ Jangan menulis pembersih HTML sendiri

Menguraikan HTML dengan benar jauh lebih rumit daripada yang terlihat: tag yang tidak ditutup, atribut tanpa kutip, entitas bertingkat, penanganan berbeda antar peramban. Pustaka mapan seperti DOMPurify sudah menutup ratusan kasus tepi yang ditemukan bertahun-tahun. Regex buatan sendiri hampir selalu bisa dilewati.

Content Security Policy

Content-Security-Policy:
    default-src 'self';
    script-src  'self' 'nonce-{ACAK}';
    style-src   'self';
    img-src     'self' data: https:;
    connect-src 'self' https://api.contoh.id;
    frame-ancestors 'none';
    base-uri    'self';
    object-src  'none'
💡 CSP adalah jaring pengaman, bukan pengganti pengodean

CSP tidak mencegah XSS terjadi — ia membatasi apa yang dapat dilakukan payload setelah berhasil disisipkan. Skrip dari domain asing diblokir, eval ditolak, dan pengiriman data curian ke server penyerang gagal karena connect-src. Kombinasi pengodean yang benar plus CSP jauh lebih kuat daripada salah satunya saja.

🚫 'unsafe-inline' pada script-src membatalkan hampir seluruh manfaat CSP

Direktif ini sering ditambahkan agar skrip inline yang sudah ada tetap berjalan. Tetapi justru skrip inline itulah bentuk utama payload XSS. Gunakan nonce yang dihasilkan acak per permintaan, atau hash untuk skrip inline yang isinya tetap.

Cookie yang Tidak Dapat Dibaca Skrip

Set-Cookie: sesi=abc123;
            HttpOnly;              // tidak terbaca document.cookie
            Secure;                // hanya melalui HTTPS
            SameSite=Lax;          // batasi pengiriman lintas situs
            Path=/;
            Max-Age=3600

Dengan HttpOnly, XSS tidak lagi otomatis berarti pencurian sesi. Penyerang masih dapat bertindak atas nama pengguna selama halaman terbuka, tetapi tidak dapat mengambil token sesi untuk dipakai di tempat lain.