Pengodean keluaran (output encoding) adalah proses mengganti karakter khusus menjadi bentuk yang aman tepat sebelum data ditulis ke sebuah konteks, misalnya mengganti < menjadi < sebelum data masuk ke isi elemen HTML. Praktik ini merupakan pertahanan utama terhadap XSS karena serangan itu sendiri lahir dari data yang diperlakukan sebagai kode. Topik ini membahas pemetaan konteks ke pengodeannya, lengkap dengan implementasinya menggunakan pustaka standar Python. Payload yang sama digunakan pada seluruh contoh, sehingga perbedaan hasil antar konteks terlihat jelas.
Mengapa konteks menentukan pengodean#
HTML, JavaScript, dan URL adalah tiga bahasa dengan tanda khusus yang berbeda. Karakter < menandai awal tag pada HTML, tanda kutip ganda menutup literal string pada JavaScript, sedangkan & dan + memiliki makna tersendiri pada URL. Data yang aman pada satu konteks dapat merusak struktur pada konteks lain, sehingga tidak ada satu pun pengodean yang cocok untuk semua tempat. Pertimbangan utama saat menulis data ke halaman bukan keamanan isinya, melainkan konteks tujuan penulisannya.
Waktu pelaksanaannya juga menentukan. Pengodean dilakukan paling akhir, yaitu tepat sebelum data masuk ke konteksnya. Data yang diodekan terlalu awal akan terkena pengodean lagi apabila nantinya ditulis sekali lagi, dan bentuk ganda itu merusak tampilan tanpa menambah keamanan apa pun. Karena pertimbangan inilah template engine modern menempatkan pengodean otomatis pada lapisan render, bukan pada lapisan pembacaan masukan.
Konteks yang dimaksud bukan hanya tempat data berada pada halaman, melainkan posisi strukturnya. Satu baris template dapat memuat teks biasa, nilai atribut, dan data skrip sekaligus, dan masing-masing menuntut pengodean yang berbeda. Pengodean HTML otomatis dari template engine menutup kebutuhan dua konteks pertama, sedangkan konteks skrip dan URL menuntut keputusan eksplisit dari penulisnya. Pemetaan pada bagian berikut menjadikan keputusan itu sistematis.
Satu payload, empat konteks#
Skrip berikut menerapkan payload yang sama, yaitu '><img src=x onerror=alert(1)>"&, pada empat konteks keluaran menggunakan fungsi pustaka standar Python. Konteks pertama adalah isi elemen HTML, kedua adalah nilai atribut, ketiga adalah literal string di dalam elemen <script>, dan keempat adalah nilai query string pada URL. Payload sengaja memuat tanda kutip tunggal, tanda kutip ganda, tanda kurung sudut, dan ampersand sekaligus, karena keempatnya mencakup karakter yang berbahaya pada semua konteks yang diuji. Jalankan skripnya, lalu bandingkan keempat hasil itu baris demi baris.
# pengodean-keluaran.py — satu payload, empat konteks keluaran.
import html
import json
import urllib.parse
payload = "'><img src=x onerror=alert(1)>\"&"
print(f"Payload asli: {payload}")
print()
print("[1] Konteks isi elemen HTML (teks di dalam body)")
print(html.escape(payload, quote=False))
print()
print("[2] Konteks atribut HTML (nilai di dalam tanda kutip)")
print(html.escape(payload))
print()
print("[3] Konteks data di dalam <script> (literal string JavaScript)")
print(json.dumps(payload))
print()
print("[4] Konteks URL (nilai query string)")
print(urllib.parse.quote(payload))Payload asli: '><img src=x onerror=alert(1)>"&
[1] Konteks isi elemen HTML (teks di dalam body)
'><img src=x onerror=alert(1)>"&
[2] Konteks atribut HTML (nilai di dalam tanda kutip)
'><img src=x onerror=alert(1)>"&
[3] Konteks data di dalam <script> (literal string JavaScript)
"'><img src=x onerror=alert(1)>\"&"
[4] Konteks URL (nilai query string)
%27%3E%3Cimg%20src%3Dx%20onerror%3Dalert%281%29%3E%22%26Keempat hasil itu berbeda satu sama lain. Pemanggilan html.escape(payload, quote=False) mengganti &, <, dan > sehingga payload tidak mampu membuka tag baru pada isi elemen. Pemanggilan html.escape dengan nilai default menambahkan ' dan ", yang wajib apabila data ditulis di dalam atribut berpetik. Fungsi json.dumps menghasilkan literal string JavaScript yang valid karena tanda kutip ganda pada payload menjadi \", sehingga payload tidak dapat keluar dari literal tersebut. Terakhir, urllib.parse.quote mengubah hampir semua karakter menjadi percent-encoding (pengodean persen) sehingga aman disisipkan ke URL.
Pemetaan konteks ke pengodean#
Aturan praktisnya dapat dirangkum dalam satu tabel. Setiap baris dipasangkan dengan alat dari pustaka standar Python agar perbaikannya dapat langsung diterapkan pada kode nyata.
| Konteks keluaran | Karakter yang diganti | Alat pustaka standar Python |
|---|---|---|
| Isi elemen HTML | &, <, > | html.escape(teks, quote=False) |
| Nilai atribut HTML | &, <, >, ", ' | html.escape(teks) |
Literal string di <script> | Tanda kutip dan karakter kontrol | json.dumps(data) |
| Nilai pada URL | Karakter non-aman | urllib.parse.quote(teks) |
OWASP merinci pengodean per konteks pada lembar panduannya, mulai dari isi elemen, atribut, JavaScript, CSS, hingga URL, dan pemetaan di atas mengikuti rincian itu untuk konteks yang paling sering digunakan. Template engine seperti Jinja2 dengan autoescape aktif melakukan pengodean HTML secara otomatis untuk setiap variabel yang dicetak. Otomatisasi itu mencakup konteks HTML saja, sehingga variabel di dalam blok <script> maupun atribut href tetap memerlukan penanganan khusus, misalnya filter |tojson pada Jinja2 untuk konteks JavaScript.
Alat pustaka standar dipilih karena perilakunya terdokumentasi dan stabil antar versi. Fungsi buatan sendiri yang hanya mengganti sebagian karakter cenderung tumbuh menjadi daftar hitam, dan kekurangannya baru terlihat setelah payload melewati karakter yang terlewat. Apabila kerangka kerja yang digunakan menyediakan mekanisme templatenya sendiri, mekanisme itu tetap diutamakan, sedangkan pemanggilan pustaka standar di atas menjadi pilihan bagi kode yang merangkai HTML secara manual.
Kesalahan umum#
Kesalahan pertama adalah melakukan pengodean di titik masukan, sehingga data yang sudah di-escape tersimpan lalu diodekan sekali lagi saat ditulis. Keluaran berikut memperlihatkan akibatnya pada teks yang sudah memuat entitas HTML.
import html
teks = "Ampersand ditulis & di dalam HTML"
print("Sudah di-escape:", html.escape(teks, quote=False))
print("Di-escape lagi :", html.escape(html.escape(teks, quote=False), quote=False))Sudah di-escape: Ampersand ditulis &amp; di dalam HTML
Di-escape lagi : Ampersand ditulis &amp;amp; di dalam HTMLData tampil rusak karena & menjadi &amp;, dan perbaikannya adalah menyimpan data mentah lalu melakukan pengodean hanya pada titik keluar. Kebiasaan semacam ini biasanya lahir dari dua lapisan yang sama-sama merasa bertanggung jawab, misalnya pustaka lama dan template baru, sehingga penempatan pengodean perlu disepakati sekali pada satu titik. Kesalahan kedua adalah mengodekan dengan alat yang salah konteks, misalnya menggunakan html.escape untuk data di dalam <script>. Penanda </script> tetap mengakhiri elemen skrip di tengah literal string mana pun karena diperiksa parser HTML sebelum JavaScript diurai, sehingga data untuk konteks itu memerlukan serialisasi JSON beserta penggantian < menjadi \u003c. Kesalahan ketiga adalah merangkai HTML secara manual dengan penggabungan string, padahal template dengan autoescape melakukan pengodean yang sama secara konsisten untuk semua variabel. Kesepakatan titik keluar itu pun berlaku ketika satu data ditulis ke beberapa konteks sekaligus.
Ringkasan#
- Pertahanan utama terhadap XSS adalah mengodekan data sesuai konteks tujuannya pada saat ditulis, bukan memfilter masukan.
- Isi elemen, atribut, data JavaScript, dan URL memiliki karakter berbahaya yang berbeda, sehingga alat pengodeannya juga berbeda.
- Pengodean dilakukan sekali saja dan selalu di titik keluar, karena pengodean ganda merusak data tanpa menambah keamanan.
Referensi#
- Cross Site Scripting Prevention Cheat Sheet — OWASP — Rincian aturan pengodean keluaran per konteks HTML, atribut, JavaScript, dan URL.
- Cross-site scripting — PortSwigger Web Security Academy — Latar eksploitasi XSS yang menjadi sasaran pengodean keluaran ini.