Dua belas bab belakangan ini membangun keamanan lapis demi lapis: validasi di gerbang masuk, encoding di gerbang keluar, rahasia yang tersuntik dari luar, kriptografi dari pustaka standar, kata sandi yang di-hash lambat, sertifikat yang diverifikasi penuh, deserialisasi yang difilter, path yang dikurung, dan dependency yang dipindai rutin. Bab penutup Section ini merangkainya menjadi satu gambar utuh — sebab keamanan aplikasi bukan kumpulan fitur yang dipasang terpisah, melainkan properti sistem yang muncul ketika lapisan-lapisan itu bekerja berurutan pada setiap permintaan. Kita akan menjalankan satu permintaan melewati seluruh lapisan, menempatkan SecurityManager yang telah pensiun ke dalam konteks historisnya, lalu menutup dengan daftar periksa minimum yang bisa Anda bawa ke proyek apa pun.

Enam Lapis yang Sudah Kita Bangun #

LapisBabPertanyaan yang dijawab
Validasi masukanInput ValidationApakah data dari luar berbentuk sah?
Encoding keluaranOutput EncodingApakah data aman di konteks tujuannya?
OtorisasiPrinsip Secure CodingApakah pengguna berhak melakukan aksi ini?
KriptografiFundamentals, Password Hashing, DigestsApakah data dan identitas dilindungi algoritme standar?
KanalTLS dan Certificate ValidationApakah lawan bicara terverifikasi dan kanal terenkripsi?
Rantai pasokDependency Vulnerability ManagementApakah komponen pihak ketiga bebas kerentanan yang diketahui?

Urutan pada tabel bukan kebetulan: validasi dan otorisasi bekerja paling depan karena murah dan gagal-cepat; kriptografi dan kanal menjaga data yang telah lolos gerbang; pemindaian dependency berjalan di luar runtime sama sekali. Defense in depth berarti satu kegagalan tidak menjadi satu kebocoran — komentar yang lolos validasi tetap di-encode saat dirender; data yang tersadap tetap terenkripsi; library yang berkerentanan tetap dibatasi hak akses akunnya.

Alur Lengkap Satu Permintaan #

Program berikut memproses dua permintaan pengiriman komentar melewati empat lapisan pertama secara berurutan — validasi, otorisasi, encoding, dan integritas. Permintaan pertama sah; permintaan kedua membawa nama pengguna yang merupakan payload path traversal:

java AlurAman.java
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class AlurAman {

    // Peran sederhana untuk contoh otorisasi.
    record Pengguna(String nama, String peran) { }

    public static void main(String[] args) throws Exception {
        proses("siti", "Saya suka <b>produk</b> ini & harganya bersahabat!");
        System.out.println();
        proses("../admin", "isi komentar");
    }

    static void proses(String nama, String komentar) throws Exception {
        System.out.println("=== Permintaan dari [" + nama + "] ===");

        // Lapis 1: validasi masukan (allowlist, fail closed).
        if (!nama.matches("[a-z]{3,16}")) {
            System.out.println(" Lapis 1 validasi  : DITOLAK - nama di luar allowlist");
            System.out.println(" Eksekusi berhenti di gerbang pertama (fail closed)");
            return;
        }
        System.out.println(" Lapis 1 validasi  : lolos - nama sesuai allowlist");

        // Lapis 2: otorisasi (peran menentukan aksi yang diizinkan).
        Pengguna pengguna = new Pengguna(nama, "anggota");
        if (!pengguna.peran().equals("anggota") && !pengguna.peran().equals("editor")) {
            System.out.println(" Lapis 2 otorisasi : DITOLAK - peran tidak berhak");
            return;
        }
        System.out.println(" Lapis 2 otorisasi : lolos - peran anggota boleh berkomentar");

        // Lapis 3: encoding keluaran (komentar akan dirender sebagai HTML).
        String komentarAman = escapeHtml(komentar);
        System.out.println(" Lapis 3 encoding  : " + komentarAman);

        // Lapis 4: integritas (tag HMAC untuk deteksi manipulasi data tersimpan).
        byte[] kunci = "kunci-demo-hmac".getBytes(StandardCharsets.UTF_8);
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(kunci, "HmacSHA256"));
        byte[] tag = mac.doFinal(komentarAman.getBytes(StandardCharsets.UTF_8));
        System.out.println(" Lapis 4 integritas: tag " + HexFormat.of().formatHex(tag));
    }

    // Encoder HTML mini dari bab Output Encoding.
    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
=== Permintaan dari [siti] ===
 Lapis 1 validasi  : lolos - nama sesuai allowlist
 Lapis 2 otorisasi : lolos - peran anggota boleh berkomentar
 Lapis 3 encoding  : Saya suka &lt;b&gt;produk&lt;/b&gt; ini &amp; harganya bersahabat!
 Lapis 4 integritas: tag c0fdcb6599244a6b5c17622452b04e23b9090cd3ae17f4c2cbcb15d66151250d

=== Permintaan dari [../admin] ===
 Lapis 1 validasi  : DITOLAK - nama di luar allowlist
 Eksekusi berhenti di gerbang pertama (fail closed)

Permintaan sah berjalan mulus melewati keempat lapisan: nama lolos allowlist, peran anggota berhak berkomentar, komentar di-encode menjadi HTML aman, dan tag HMAC tercatat untuk mendeteksi manipulasi data tersimpan. Permintaan kedua tidak pernah mencapai lapisan kedua — nama ../admin gagal di gerbang pertama dan eksekusi berhenti. Pola early return pada method proses adalah bentuk paling murni dari fail closed: data yang gagal satu pemeriksaan tidak mendapat kesempatan kedua.

Bedah singkat untuk keputusan arsitektur method ini:

  1. Baris 22-26 (gerbang pertama + return): validasi ditempatkan paling depan dan paling murah — satu panggilan matches() — sehingga permintaan bermasalah dibuang sebelum menyentuh alokasi objek maupun logika bisnis. Urutan lapisan yang tepat menambah keamanan tanpa menambah biaya.
  2. Baris 33-36 (otorisasi eksplisit): peran diperiksa di dalam aksi yang diminta, bukan sekali di login. Pengguna sah sekalipun tetap melewati pemeriksaan hak per aksi — pola yang mencegah privilege escalation lewat endpoint yang lupa dilindungi.
  3. Baris 39-41 (encoding tepat sebelum keluar): komentar disimpan dalam bentuk asli dan di-encode saat akan dirender. Kunci HMAC dihitung atas versi ter-encode agar tag mendampingi persis data yang tampil.

SecurityManager Pensiun JEP 411 #

Selama dua dekade, JDK membawa satu komponen yang menjanjikan “sandbox” kode Java: SecurityManager, yang memeriksa izin berkas, jaringan, dan runtime lewat policy file. Praktik membuktikan desain itu sulit dipelihara — semantic yang tersirat merata di seluruh pustaka JDK, biaya kinerja yang tidak bisa dihapuskan, dan model izin yang terlalu kasar untuk aplikasi modern. JEP 411 menandai SecurityManager sebagai deprecated for removal sejak Java 17, dan kode baru tidak boleh lagi membangun pertahanan di atasnya.

Penggantinya bukan satu API, melainkan pergeseran filosofi: isolasi dilakukan oleh lapisan sistem operasi dan platform, bukan oleh JVM dari dalam. Proses aplikasi berjalan sebagai akun tanpa hak lebih (least privilege di level OS), container membatasi berkas, jaringan, dan sumber daya yang terlihat proses, dan systemd serta profil keamanan sejenis memagari akses kernel. Sebutan modern untuk pendekatan ini adalah process isolation — di luar JVM, dan justru karena itu sulit dilewati dari dalam kode Java yang dikompromikan.

Daftar Periksa Minimum #

Untuk proyek Java baru, daftar berikut adalah titik awal yang bisa dilengkapi audit menyeluruh:

  • Seluruh data lintas trust boundary divalidasi allowlist sebelum dipakai — Integer.parseInt untuk angka, regex ketat untuk identifier.
  • Seluruh query SQL memakai PreparedStatement dengan parameter terbinding; string query tidak pernah digabung dengan data pengguna.
  • Seluruh keluaran ke HTML, URL, dan JavaScript di-encode sesuai konteks tepat sebelum dikirim.
  • Rahasia hanya masuk lewat variabel lingkungan atau secret manager; .gitignore menutup berkas lokal; secret scanning terpasang di pipeline.
  • Kata sandi disimpan dengan PBKDF2/bcrypt/Argon2id plus salt unik; perbandingan memakai MessageDigest.isEqual().
  • Token dan IV dibangkitkan SecureRandom; IV GCM tidak pernah dipakai ulang dengan kunci yang sama.
  • Koneksi keluar memakai HttpClient bawaan dengan validasi sertifikat dan hostname aktif — tanpa trust manager yang dimatikan.
  • Deserialisasi data luar difilter ObjectInputFilter; format data dipilih bila format objek tidak dibutuhkan.
  • Akses berkas pengguna lewat resolve + normalize + startsWith dengan basis terkurung.
  • Dependency dipindai CVE secara terjadwal; versi dikunci eksplisit; SBOM dihasilkan tiap rilis.
  • Log tidak pernah memuat rahasia, token, atau data pribadi; kegagalan keamanan dicatat dan dipantau (pemantauan adalah lapis terakhir yang tidak kita bahas tuntas — latihan praktiknya menunggu bab Deployment dan Production).

Dengan ini Section Security usai — dan bersamanya seluruh fondasi menulis Java yang benar, efisien, dan aman telah berdiri. Pengetahuan selanjutnya bukan lagi tentang bahasanya, melainkan tentang membangun: aplikasi desktop, aplikasi web, dan sistem terdistribusi yang menjadi rumah bagi semua teknik yang telah kita kuasai.

Referensi #

Berkas Sumber

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

AlurAman.java

Unduh berkas
java
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class AlurAman {

    // Peran sederhana untuk contoh otorisasi.
    record Pengguna(String nama, String peran) { }

    public static void main(String[] args) throws Exception {
        proses("siti", "Saya suka <b>produk</b> ini & harganya bersahabat!");
        System.out.println();
        proses("../admin", "isi komentar");
    }

    static void proses(String nama, String komentar) throws Exception {
        System.out.println("=== Permintaan dari [" + nama + "] ===");

        // Lapis 1: validasi masukan (allowlist, fail closed).
        if (!nama.matches("[a-z]{3,16}")) {
            System.out.println(" Lapis 1 validasi  : DITOLAK - nama di luar allowlist");
            System.out.println(" Eksekusi berhenti di gerbang pertama (fail closed)");
            return;
        }
        System.out.println(" Lapis 1 validasi  : lolos - nama sesuai allowlist");

        // Lapis 2: otorisasi (peran menentukan aksi yang diizinkan).
        Pengguna pengguna = new Pengguna(nama, "anggota");
        if (!pengguna.peran().equals("anggota") && !pengguna.peran().equals("editor")) {
            System.out.println(" Lapis 2 otorisasi : DITOLAK - peran tidak berhak");
            return;
        }
        System.out.println(" Lapis 2 otorisasi : lolos - peran anggota boleh berkomentar");

        // Lapis 3: encoding keluaran (komentar akan dirender sebagai HTML).
        String komentarAman = escapeHtml(komentar);
        System.out.println(" Lapis 3 encoding  : " + komentarAman);

        // Lapis 4: integritas (tag HMAC untuk deteksi manipulasi data tersimpan).
        byte[] kunci = "kunci-demo-hmac".getBytes(StandardCharsets.UTF_8);
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(kunci, "HmacSHA256"));
        byte[] tag = mac.doFinal(komentarAman.getBytes(StandardCharsets.UTF_8));
        System.out.println(" Lapis 4 integritas: tag " + HexFormat.of().formatHex(tag));
    }

    // Encoder HTML mini dari bab Output Encoding.
    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();
    }
}