Data yang sudah aman di dalam sistem dapat menjadi berbahaya saat dikirim ke lapisan presentasi seperti browser atau terminal. Sebuah string <script>alert(1)</script> yang valid sebagai teks akan dijalankan sebagai kode JavaScript bila ditempel langsung ke halaman HTML tanpa encoding. Prinsip output encoding menyatakan: setiap kali data berpindah dari satu konteks ke konteks lain (database ke HTML, string ke URL, objek ke JSON), data harus di-encode sesuai aturan konteks tujuan. Pada chapter ini kita akan mempraktikkan tiga jenis encoding utama—HTML entity, URL, dan JSON string—serta melihat bagaimana kombinasi keduanya mencegah serangan cross-site scripting (XSS).

HTML Entity Encoding #

HTML mengenali karakter khusus seperti <, >, ", ', dan & sebagai penanda struktur atau entitas. Saat karakter-karakter ini muncul dalam data pengguna dan ditempel langsung ke halaman HTML, browser salah menginterpretasikannya sebagai bagian dari markup. Solusinya adalah mengganti setiap karakter khusus dengan entitas HTML yang sesuai.

kotlin Peta pengganti karakter HTML
private val HTML_ESCAPES: Map<Char, String> = mapOf(
    '&' to "&amp;",
    '<' to "&lt;",
    '>' to "&gt;",
    '"' to "&quot;",
    '\'' to "&#39;"
)

Karakter & harus di-encode pertama kali; bila tidak, hasil encoding karakter lain seperti &lt; akan rusak menjadi &amp;lt;. Urutan penggantian dalam implementasi di atas memproses per-karakter satu per satu sehingga kasus tumpang tindih tidak terjadi.

URL Encoding #

URL hanya boleh berisi karakter dari subset ASCII yang aman: huruf, angka, serta sebagian tanda baca. Karakter seperti spasi, &, =, +, atau non-ASCII harus di-encode menjadi %XX agar tidak salah ditafsirkan sebagai pemisah parameter. JDK menyediakan URLEncoder.encode yang menangani konversi ini sesuai charset yang ditentukan.

kotlin Contoh: URL encoding dengan UTF-8
import java.net.URLEncoder
import java.nio.charset.StandardCharsets

fun urlEncode(input: String): String =
    URLEncoder.encode(input, StandardCharsets.UTF_8)

Penting untuk selalu menentukan StandardCharsets.UTF_8 eksplisit. Beberapa overload lama menggunakan charset default platform yang dapat berbeda antara mesin pengembangan dan produksi.

JSON String Encoding #

JSON membatasi karakter yang boleh muncul di dalam string literal. Karakter seperti ", \, dan karakter kontrol (kode di bawah 0x20) harus di-escape. Tanpa encoding yang benar, payload berbahaya dapat keluar dari string literal dan memodifikasi struktur JSON. Encoding JSON dalam kode di bawah mengikuti RFC 8259.

kotlin
// output_encoding.kt
// HTML entity encoding, URL encoding, JSON encoding. Prevent XSS.

import java.net.URLEncoder
import java.net.URLDecoder
import java.nio.charset.StandardCharsets

// --- HTML entity encoding (manual whitelist approach using replacement map) ---
private val HTML_ESCAPES: Map<Char, String> = mapOf(
    '&' to "&amp;",
    '<' to "&lt;",
    '>' to "&gt;",
    '"' to "&quot;",
    '\'' to "&#39;"
)

fun htmlEncode(input: String): String {
    val sb = StringBuilder(input.length + 16)
    for (ch in input) {
        sb.append(HTML_ESCAPES[ch] ?: ch)
    }
    return sb.toString()
}

// --- URL encoding using java.net.URLEncoder (handles spaces, reserved chars) ---
fun urlEncode(input: String): String =
    URLEncoder.encode(input, StandardCharsets.UTF_8)

fun urlDecode(input: String): String =
    URLDecoder.decode(input, StandardCharsets.UTF_8)

// --- JSON string encoding (RFC 8259). Wraps with double quotes. ---
fun jsonStringEncode(input: String): String {
    val sb = StringBuilder(input.length + 16)
    sb.append('"')
    for (ch in input) {
        when (ch) {
            '"' -> sb.append("\\\"")
            '\\' -> sb.append("\\\\")
            '\n' -> sb.append("\\n")
            '\r' -> sb.append("\\r")
            '\t' -> sb.append("\\t")
            '\b' -> sb.append("\\b")
            '\u000C' -> sb.append("\\f")
            else -> {
                if (ch.code < 0x20) {
                    sb.append("\\u%04x".format(ch.code))
                } else {
                    sb.append(ch)
                }
            }
        }
    }
    sb.append('"')
    return sb.toString()
}

// --- Demonstrate XSS prevention in HTML template ---
fun renderProfile(nama: String, bio: String): String {
    val amanNama = htmlEncode(nama)
    val amanBio = htmlEncode(bio)
    return """<div class="profile">
  |  <h1>$amanNama</h1>
  |  <p>$amanBio</p>
  |</div>""".trimMargin()
}

fun main() {
    // HTML encoding demo
    val payloadHtml = """<script>alert('XSS')</script>"""
    println("htmlEncode(payload):")
    println(htmlEncode(payloadHtml))
    println()

    // URL encoding demo
    val q = "kotlin tutorial & framework"
    val encoded = urlEncode(q)
    println("urlEncode('$q') = $encoded")
    println("urlDecode(encoded) = ${urlDecode(encoded)}")
    println()

    // JSON string encoding
    val payloadJson = """baris1\nbaris2 "kutip" \backslash"""
    println("jsonStringEncode(payload):")
    println(jsonStringEncode(payloadJson))
    println()

    // Render profile (XSS prevention)
    val nama = "Budi"
    val bio = """Saya suka <b>Kotlin</b> & JavaScript."""
    println("renderProfile('$nama', '$bio'):")
    println(renderProfile(nama, bio))
}

Output yang Dihasilkan:

text
htmlEncode(payload):
&lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt;

urlEncode('kotlin tutorial & framework') = kotlin+tutorial+%26+framework
urlDecode(encoded) = kotlin tutorial & framework

jsonStringEncode(payload):
"baris1\\nbaris2 \"kutip\" \\backslash"

renderProfile('Budi', 'Saya suka <b>Kotlin</b> & JavaScript.'):
<div class="profile">
  <h1>Budi</h1>
  <p>Saya suka &lt;b&gt;Kotlin&lt;/b&gt; &amp; JavaScript.</p>
</div>

Mari kita bedah bagian penting dari kode di atas:

  1. htmlEncode: mengiterasi setiap karakter dan menggantinya bila ada di HTML_ESCAPES. Karakter selain lima kunci tersebut dibiarkan apa adanya. Pendekatan ini aman untuk konteks teks HTML, namun tidak cukup untuk konteks atribut—misalnya atribut event handler seperti onclick memerlukan aturan tambahan.
  2. urlEncode/urlDecode: menggunakan URLEncoder/URLDecoder bawaan JDK. Spasi diubah menjadi + (bukan %20); tanda & menjadi %26. Output urlDecode mengembalikan string asli karena encoding bersifat reversibel.
  3. jsonStringEncode: membungkus string dengan tanda kutip ganda. Karakter kontrol dengan kode di bawah 0x20 di-escape menggunakan \uXXXX agar tetap valid sesuai RFC 8259. Karakter " dan \ menggunakan escape sequence singkat seperti \" dan \\.
  4. renderProfile: menggabungkan HTML encoding dengan template literal Kotlin. trimMargin() menjaga indentasi tetap rapi. Hasil menunjukkan bahwa <b>Kotlin</b> dan & dalam bio berhasil di-encode menjadi &lt;b&gt;Kotlin&lt;/b&gt; dan &amp;.

Pencegahan XSS dengan Kombinasi Encoding #

Serangan XSS terjadi saat penyerang menyisipkan kode JavaScript yang dijalankan browser pengguna yang terdampak. Pencegahannya bergantung pada konteks tempat data ditempel:

KonteksEncoding wajibContoh
Teks HTMLEntity encode <, >, &, ", '<div>{nama}</div>
Atribut HTMLSama dengan teks HTML, plus pastikan kutip atribut<input value="{nama}">
URL parameterURL encode &, =, +, spasi?q={query}
Inline JavaScriptJSON encode<script>var u = {data};</script>
CSSHex escape, batasi karaktercolor: {warna};

Tabel di atas menunjukkan bahwa tidak ada satu encoding yang cocok untuk semua konteks. Sebuah string yang sudah di-entity encode untuk HTML tetap perlu di-URL encode bila ditempel ke atribut href.

Menggunakan Library yang Teruji #

Meskipun kode manual di atas cukup untuk pembelajaran, pada produksi sebaiknya gunakan library yang sudah teruji. Untuk HTML, owasp/java-html-sanitizer menangani kasus edge case yang sulit. Untuk JSON, kotlinx.serialization menghasilkan string JSON yang aman secara otomatis. Library ini menghilangkan risiko lupa meng-encode karakter tertentu.

Referensi Lanjutan #

  • OWASP Attacks — katalog serangan OWASP termasuk XSS yang dicegah melalui output encoding

Penutup #

Output encoding melengkapi validasi input: bahkan bila input sudah lolos whitelist, data tetap perlu di-encode saat dikirim ke konteks baru. Kombinasi HTML entity encoding, URL encoding, dan JSON encoding menutup celah XSS pada titik-titik umum. Chapter berikutnya akan membahas secrets management—bagaimana mengelola kredensial dan kunci API tanpa menempelnya langsung di dalam source code.