Validasi masukan menyaring apa yang masuk; output encoding menjaga apa yang keluar. Keduanya menjawab persoalan yang sama dari arah berlawanan: data pengguna adalah sekumpulan byte yang netral, dan bahaya muncul hanya ketika byte-byte itu ditafsirkan sebagai kode oleh tujuannya — browser menafsirkan <script> sebagai JavaScript, parser SQL menafsirkan '-- sebagai pembatas string, dan parser URL menafsirkan & sebagai pemisah parameter. Output encoding memutus tafsiran itu: setiap karakter yang bermakna khusus di konteks tujuan diganti wujud lain yang dijamin tampil sebagai teks biasa. Bab ini membangun encoder HTML mini, memakai URLEncoder untuk konteks URL, lalu memperlihatkan pola PreparedStatement untuk konteks SQL.

Konteks Menentukan Encoding #

Kata kunci bab ini adalah konteks. String yang sama membutuhkan perlakuan berbeda tergantung ke mana string itu berangkat:

Konteks tujuanContoh karakter berbahayaEncoding yang tepat
Badan HTML / atribut HTML<, >, &, ", 'Entity escape: &lt;, &amp;, &#39;
URL / parameter query&, =, ?, spasiPercent-encoding: %26, %3D, +
SQL', --, ;Parameter terbinding (PreparedStatement), bukan string escape
JavaScript di dalam HTML<, \, backtickJSON + escape JavaScript, bukan entity HTML

Tabel ini menjelaskan mengapa “sanitasi universal” tidak ada: tanda petik ' harus menjadi &#39; untuk HTML, tetapi untuk SQL tanda petik tidak di-escape sama sekali — nilai dikirim sebagai parameter terpisah dari teks query. Memakai encoder yang salah untuk konteksnya sama saja dengan tidak meng-encode.

Encoder HTML Mini #

Mari kita bangun encoder HTML dari nol untuk melihat mekanismenya. Lima karakter mendapat perlakuan khusus: &, <, >, ", dan ' — kelimanya memiliki makna sintaksis di dokumen HTML, dan kelima-limanya kerap muncul di data pengguna seperti komentar atau nama tampilan. Program berikut meng-encode komentar yang memuat payload XSS sungguhan lalu membandingkan wujud sebelum dan sesudah:

java EncoderMini.java
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

public class EncoderMini {

    public static void main(String[] args) {
        String komentar = "Skrip uji: <script>alert('XSS')</script> dari Dewi & Budi";

        System.out.println("=== Konteks HTML: entity escape ===");
        System.out.println("Asli : " + komentar);
        System.out.println("Aman : " + escapeHtml(komentar));

        System.out.println();
        System.out.println("=== Konteks URL: percent-encoding ===");
        String kueri = "kota=Bandung & cuaca=cerah";
        System.out.println("Asli : " + kueri);
        System.out.println("Aman : " + URLEncoder.encode(kueri, StandardCharsets.UTF_8));

        System.out.println();
        System.out.println("=== Konteks atribut HTML yang bernilai URL ===");
        System.out.println("Aman : redirect.jsp?next="
                + URLEncoder.encode("dashboard?id=7&tab=saldo", StandardCharsets.UTF_8));
    }

    // Encoder HTML minimal: lima karakter yang bermakna khusus di HTML
    // diganti entity-nya. Urutan penggantian & lebih dulu penting agar
    // hasil penggantian tidak di-escape ulang.
    static String escapeHtml(String teks) {
        StringBuilder hasil = new StringBuilder(teks.length() + 16);
        for (int i = 0; i < teks.length(); i++) {
            switch (teks.charAt(i)) {
                case '&' -> hasil.append("&amp;");
                case '<' -> hasil.append("&lt;");
                case '>' -> hasil.append("&gt;");
                case '"' -> hasil.append("&quot;");
                case '\'' -> hasil.append("&#39;");
                default -> hasil.append(teks.charAt(i));
            }
        }
        return hasil.toString();
    }
}

Output yang Dihasilkan:

text
=== Konteks HTML: entity escape ===
Asli : Skrip uji: <script>alert('XSS')</script> dari Dewi & Budi
Aman : Skrip uji: &lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt; dari Dewi &amp; Budi

=== Konteks URL: percent-encoding ===
Asli : kota=Bandung & cuaca=cerah
Aman : kota%3DBandung+%26+cuaca%3Dcerah

=== Konteks atribut HTML yang bernilai URL ===
Aman : redirect.jsp?next=dashboard%3Fid%3D7%26tab%3Dsaldo

Perhatikan hasil pada bagian pertama: payload <script>alert('XSS')</script> berubah menjadi &lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt;. Ketika browser membaca teks hasil encoding itu, browser menampilkan karakter < sebagai teks biasa — browser tidak pernah melihat tag <script> sama sekali, sehingga skrip tidak tereksekusi. Serangan tidak ditolak; serangan dinetralkan menjadi teks tak berdaya.

Bedah baris-per-baris untuk method escapeHtml:

  1. Baris 28 (StringBuilder hasil = ...): hasil dibangun di buffer baru, bukan String.replace() berantai yang mengalokasikan string baru per karakter. Untuk teks panjang, pola satu-sapuan ini jauh lebih efisien.
  2. Baris 29-37 (switch per karakter): switch expression Java modern memetakan lima karakter berbahaya ke entity-nya; karakter lain disalin apa adanya. Himpunan yang di-encode sengaja kecil dan eksplisit — encoder yang meng-encode “semuanya” justru membuat tampilan rusak tanpa menambah keamanan.
  3. Baris 30 (case '&' -> hasil.append("&amp;")): karakter & wajib ikut di-encode meskipun tampak tidak berbahaya, karena & adalah pembuka entity reference itu sendiri — &amp; yang tidak di-encode bisa berubah makna saat browser menafsirkan ulang. Meng-encode & menjaga hasil encoding tetap stabil bila di-encode lagi di masa depan (double-encoding aman: &amp; menjadi &amp;amp; yang tetap tampil sebagai &amp;).

Percent-Encoding untuk URL #

Bagian kedua output memakai URLEncoder dari pustaka standar — tidak perlu menulis sendiri. Charset UTF-8 diteruskan eksplisit agar hasil konsisten lintas platform. Perhatikan transformasinya: spasi menjadi +, & menjadi %26, = menjadi %3D. Nilai kota=Bandung & cuaca=cerah yang semula akan dipecah parser menjadi tiga parameter terpisah kini menjadi satu nilai utuh yang aman dikirim sebagai nilai parameter.

Bagian ketiga memperlihatkan jebakan yang lebih halus: URL yang disisipkan ke dalam atribut HTML — misalnya parameter redirect next — harus di-encode sebagai URL lengkap, termasuk karakter ? dan = yang menjadi %3F dan %3D. Tanpa encoding, penyerang bisa mengarahkan parameter next ke dashboard?id=7&tab=isAdmin lalu menyuntik parameter ekstra ke halaman tujuan. Encoding memastikan seluruh nilai diperlakukan sebagai satu blob data, bukan struktur URL yang bisa dimanipulasi.

SQL dan Parameter Terbinding #

Konteks SQL menuntut pendekatan yang berbeda secara fundamental: bukan meng-escape string, melainkan memisahkan kode dari data. PreparedStatement mengirim teks query terlebih dahulu ke database tanpa nilai apa pun — parser SQL selesai menafsirkan strukturnya — lalu nilai terkirim sebagai parameter terbinding yang tidak pernah ditafsirkan ulang:

java Pola PreparedStatement dengan parameter terbinding
// query dan data tidak pernah bersentuhan sebagai satu string
String sql = "SELECT nama, saldo FROM rekening WHERE no_rekening = ? AND status = ?";
try (java.sql.PreparedStatement ps = koneksi.prepareStatement(sql)) {
    ps.setString(1, nomorRekening);          // nilai dari pengguna
    ps.setString(2, statusYangsah);          // nilai internal
    try (java.sql.ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            // proses baris hasil
        }
    }
}

Nilai ' OR '1'='1 yang dikirim pengguna menjadi isi parameter — dibandingkan literal dengan isi kolom — dan tidak pernah memiliki kesempatan ditafsirkan sebagai logika SQL. Pola ini berlaku juga untuk IN (parameter per elemen) dan LIKE (wildcard % ditangani setelah binding, bila memang diizinkan). Tiga lapis pertahanan dari Bab Prinsip Secure Coding terpenuhi sekaligus: validasi masukan menyaring bentuk, PreparedStatement memisahkan kode dari data, dan hak akses database membatasi dampak kegagalan.

Data masuk telah divalidasi; data keluar kini aman di semua konteks. Lapisan berikutnya menyangkut benda yang paling sering bocor bukan lewat serangan teknis, melainkan lewat kelalaian manusia: rahasia — kunci API, kata sandi database, token layanan. Bab Secrets Management mengajarkan cara menyimpan dan menyuntikkan rahasia tanpa pernah menuliskannya di dalam kode.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

EncoderMini.java

Unduh berkas
java
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

public class EncoderMini {

    public static void main(String[] args) {
        String komentar = "Skrip uji: <script>alert('XSS')</script> dari Dewi & Budi";

        System.out.println("=== Konteks HTML: entity escape ===");
        System.out.println("Asli : " + komentar);
        System.out.println("Aman : " + escapeHtml(komentar));

        System.out.println();
        System.out.println("=== Konteks URL: percent-encoding ===");
        String kueri = "kota=Bandung & cuaca=cerah";
        System.out.println("Asli : " + kueri);
        System.out.println("Aman : " + URLEncoder.encode(kueri, StandardCharsets.UTF_8));

        System.out.println();
        System.out.println("=== Konteks atribut HTML yang bernilai URL ===");
        System.out.println("Aman : redirect.jsp?next="
                + URLEncoder.encode("dashboard?id=7&tab=saldo", StandardCharsets.UTF_8));
    }

    // Encoder HTML minimal: lima karakter yang bermakna khusus di HTML
    // diganti entity-nya. Urutan penggantian & lebih dulu penting agar
    // hasil penggantian tidak di-escape ulang.
    static String escapeHtml(String teks) {
        StringBuilder hasil = new StringBuilder(teks.length() + 16);
        for (int i = 0; i < teks.length(); i++) {
            switch (teks.charAt(i)) {
                case '&' -> hasil.append("&amp;");
                case '<' -> hasil.append("&lt;");
                case '>' -> hasil.append("&gt;");
                case '"' -> hasil.append("&quot;");
                case '\'' -> hasil.append("&#39;");
                default -> hasil.append(teks.charAt(i));
            }
        }
        return hasil.toString();
    }
}