Kode jaringan membuka pintu yang tidak dimiliki kode biasa: input dari pengguna bisa berubah menjadi alamat tujuan, rahasia melintas di kabel, dan setiap koneksi adalah kesempatan bagi pihak lain untuk berbuat. Kebocoran data besar hampir selalu berujung pada satu dari tiga pola sederhana — aplikasi percaya alamat dari input, rahasia tercecer di log, atau validasi dilewati demi kenyamanan. Kabar baiknya: pertahanan untuk ketiganya tidak rumit, hanya butuh disiplin. Chapter penutup Section ini merangkum praktik aman yang menaungi seluruh materi sebelumnya — validasi alamat dengan allowlist, penutupan celah SSRF, penanganan rahasia, dan prinsip fail-closed — dalam bentuk kode yang bisa diuji.

Validasi Alamat dari Input Pengguna #

Skenario umumnya: aplikasi kita menerima URL atau nama host dari pengguna — misalnya layanan pengambil gambar, webhook keluar, atau integrasi yang dapat dikonfigurasi. Prinsip pertama keamanan jaringan adalah jangan pernah membiarkan input menentukan tujuan secara bebas. Pola yang benar adalah allowlist: daftar tujuan yang diizinkan disimpan di sisi aplikasi, dan input hanya lolos bila seluruh unsurnya cocok — scheme yang diharapkan dan host yang tercantum.

Validasinya dilakukan pada komponen hasil parsing URI, bukan pada string mentah, karena string yang tampak meyakinkan bisa menyimpan kejutan. Perhatikan kasus https://[email protected]/ — di hadapan mata biasa tampak seperti alamat sah, tetapi parser membacanya berbeda: bagian sebelum @ adalah userinfo, dan host yang sebenarnya adalah penyerang.example.net. Mari kita buktikan dengan kode:

import java.net.URI;
import java.util.Locale;
import java.util.Set;

public class ValidasiHost {
    // Allowlist: satu-satunya tujuan yang boleh dihubungi aplikasi
    static final Set<String> HOST_DIIZINKAN = Set.of("api.example.com", "cdn.example.com");

    static String periksa(String teksUrl) {
        URI uri;
        try {
            uri = URI.create(teksUrl);
        } catch (IllegalArgumentException e) {
            return "DITOLAK (URI tidak valid)";
        }
        if (!"https".equalsIgnoreCase(uri.getScheme())) {
            return "DITOLAK (scheme bukan https)";
        }
        String host = uri.getHost();
        if (host == null) {
            return "DITOLAK (host kosong)";
        }
        if (!HOST_DIIZINKAN.contains(host.toLowerCase(Locale.ROOT))) {
            return "DITOLAK (host tidak ada di allowlist)";
        }
        return "DIIZINKAN";
    }

    public static void main(String[] args) {
        String[] kandidat = {
                "https://api.example.com/v1/produk",
                "https://CDN.example.com/gambar/1.png",
                "http://api.example.com/v1/produk",
                "https://penyerang.example.net/v1/produk",
                "https://[email protected]/v1/produk",
                "https://192.168.1.10/admin",
                "file:///etc/hosts",
        };
        for (String url : kandidat) {
            System.out.printf("%-56s -> %s%n", url, periksa(url));
        }
    }
}

Output yang Dihasilkan:

text
https://api.example.com/v1/produk                        -> DIIZINKAN
https://CDN.example.com/gambar/1.png                     -> DIIZINKAN
http://api.example.com/v1/produk                         -> DITOLAK (scheme bukan https)
https://penyerang.example.net/v1/produk                  -> DITOLAK (host tidak ada di allowlist)
https://[email protected]/v1/produk  -> DITOLAK (host tidak ada di allowlist)
https://192.168.1.10/admin                               -> DITOLAK (host tidak ada di allowlist)
file:///etc/hosts                                        -> DITOLAK (scheme bukan https)

Mari kita telaah pemeriksaannya baris demi baris:

  1. Baris 15-20 (try { uri = URI.create(...) }): parsing dijalankan lebih dulu sehingga input rusak tertolak di muka. Kegagalan parsing adalah jawaban “tidak”, bukan galat untuk diperbaiki.
  2. Baris 21-23 (pemeriksaan scheme): hanya https diizinkan. Satu syarat ini sekaligus menutup file://, http:// tanpa enkripsi, dan scheme eksotis lainnya — terlihat pada baris file:///etc/hosts yang tertolak di tahap ini.
  3. Baris 27-33 (pemeriksaan host): host dibandingkan terhadap allowlist setelah dinormalkan ke huruf kecil dengan Locale.ROOT, karena URI.getHost() mempertahankan kapitalisasi asli sementara nama domain tidak peka kapital. Baris CDN.example.com yang tampak berbeda tetap diizinkan berkat normalisasi ini.
  4. Baris 44 (kandidat @penyerang): pembuktian trik userinfo — host hasil parsing adalah penyerang.example.net, sehingga validasi menolaknya meski awalan URL meniru domain kami. Bandingkan dengan pemeriksaan string startsWith("https://api.example.com") yang akan tertipu oleh kandidat ini.
  5. Baris 45 (kandidat 192.168.1.10): alamat IP internal tidak tercantum di allowlist, jadi tertolak — allowlist menutup kelas masalah ini tanpa perlu mendaftar pola larangan satu per satu.

SSRF (Server-Side Request Forgery) terjadi ketika pihak luar memaksa server kita menghubungi alamat yang tidak semestinya dihubungi — layanan metadata cloud, panel admin internal, atau database yang hanya mendengarkan di jaringan privat. Pertahanan berlapis terhadapnya: allowlist seperti di atas sebagai lapis pertama, dan pemeriksaan alamat hasil resolusi sebagai lapis kedua — sebab nama domain yang sah pun bisa menunjuk ke alamat internal.

Class InetAddress menyediakan pemeriksaan kelas alamat yang bekerja lokal tanpa jaringan: isLoopbackAddress() untuk 127.0.0.0/8, isSiteLocalAddress() untuk rentang privat RFC 1918 (10/8, 172.16/12, 192.168/16), isLinkLocalAddress() untuk 169.254/16, dan isAnyLocalAddress() untuk 0.0.0.0. Program berikut memeriksa rangkaian kandidat:

java PenjagaSsrf.java
import java.net.InetAddress;

public class PenjagaSsrf {
    // Tolak alamat internal: kandidat apapun yang menunjuk ke dalam jaringan sendiri
    static String periksaAlamat(InetAddress alamat) {
        if (alamat.isLoopbackAddress() || alamat.isAnyLocalAddress()
                || alamat.isSiteLocalAddress() || alamat.isLinkLocalAddress()) {
            return "DITOLAK (alamat internal)";
        }
        return "DIIZINKAN";
    }

    public static void main(String[] args) throws Exception {
        // Alamat numerik murni: tidak ada pencarian DNS sama sekali
        String[] kandidat = {
                "127.0.0.1", "10.0.0.5", "192.168.1.10",
                "169.254.1.1", "0.0.0.0", "93.184.216.34"
        };
        for (String teks : kandidat) {
            InetAddress alamat = InetAddress.getByName(teks);
            System.out.printf("%-15s -> %s%n", teks, periksaAlamat(alamat));
        }
    }
}

Output yang Dihasilkan:

text
127.0.0.1       -> DITOLAK (alamat internal)
10.0.0.5        -> DITOLAK (alamat internal)
192.168.1.10    -> DITOLAK (alamat internal)
169.254.1.1     -> DITOLAK (alamat internal)
0.0.0.0         -> DITOLAK (alamat internal)
93.184.216.34   -> DIIZINKAN

Method periksaAlamat menjalankan empat pemeriksaan dalam satu kondisi gabungan: loopback, any-local, site-local, dan link-local — keempatnya menandai alamat yang mengarah ke dalam. Pada contoh di atas, hanya 93.184.216.34 (alamat publik) yang lolos. Dalam aplikasi nyata, pemeriksaan ini dijalankan terhadap hasil InetAddress.getByName(host) setelah allowlist scheme-host lolos; karena getByName untuk teks numerik murni tidak melakukan pencarian DNS, contoh kita berjalan sepenuhnya offline dan deterministik.

Rahasia, Log, dan Transport #

Rahasia aplikasi — token API, kredensial, header Authorization — menghadapi dua risiko klasik: transport tanpa enkripsi dan log yang terlalu rajin. Aturan pertamanya tegas: data sensitif hanya melintas lewat TLS. Skema https bukan hiasan opsional; validasi allowlist pada bagian sebelumnya sengaja menolak http:// karena alasan ini. Aturan keduanya sama tegasnya: jangan pernah mencetak rahasia ke log, termasuk pada blok penanganan galat. Stack trace dari koneksi gagal berisi URI lengkap; jika kredensial menempel pada query string atau header, satu baris log yang kurang hati-hati membocorkannya ke berkas log yang dikelola banyak pihak.

Kebiasaan konkret yang bisa langsung diterapkan:

  • Simpan kredensial di variabel lingkungan atau vault, tampilkan sebagai placeholder YOUR_API_TOKEN di dokumentasi dan contoh kode.
  • Redaksi header sebelum mencatat: catat ada-tidaknya Authorization, bukan isinya.
  • Kirim kredensial lewat header, bukan query string — URL masuk ke log akses server tujuan.
  • Set connectTimeout dan timeout pada semua permintaan keluar — koneksi yang menggantung adalah pintu kehabisan resource, dan kita sudah melengkapi polanya pada Chapter Timeouts dan Retries.

Fail Closed sebagai Prinsip Penutup #

Semua pemeriksaan pada chapter ini berbagi satu filosofi: saat pemeriksaan tidak bisa memutuskan, jawabannya “tolak”. Validasi host gagal mengurai → tolak. Scheme tidak dikenal → tolak. Alamat tidak bisa diverifikasi → tolak. Kebalikannya — fail open, meloloskan apa pun yang tidak jelas salahnya — terasa ramah saat pengembangan dan mahal di produksi, karena penyerang justru mencari kasus yang tidak jelas. Perlakukan setiap komponen jaringan dengan asumsi yang sama seperti HttpURLConnection lama memperlakukan kita: sambungan bisa gagal kapan saja, data bisa berisi apa saja, dan kode kita yang bertanggung jawab menutup pintu yang tidak seharusnya terbuka.

Dengan ini perjalanan Section Networking tuntas: dari anatomi alamat dan socket mentah, naik ke HTTP modern, lalu diamankan dengan TLS dan disiplin keamanan. Keterampilan berikutnya yang paling sering berpasangan dengan jaringan adalah mengelola data yang tersimpan — koneksi database, query, dan transaksi — pokok bahasan Section Database Programming.

Referensi #

Berkas Sumber

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

PenjagaSsrf.java

Unduh berkas
java
import java.net.InetAddress;

public class PenjagaSsrf {
    // Tolak alamat internal: kandidat apapun yang menunjuk ke dalam jaringan sendiri
    static String periksaAlamat(InetAddress alamat) {
        if (alamat.isLoopbackAddress() || alamat.isAnyLocalAddress()
                || alamat.isSiteLocalAddress() || alamat.isLinkLocalAddress()) {
            return "DITOLAK (alamat internal)";
        }
        return "DIIZINKAN";
    }

    public static void main(String[] args) throws Exception {
        // Alamat numerik murni: tidak ada pencarian DNS sama sekali
        String[] kandidat = {
                "127.0.0.1", "10.0.0.5", "192.168.1.10",
                "169.254.1.1", "0.0.0.0", "93.184.216.34"
        };
        for (String teks : kandidat) {
            InetAddress alamat = InetAddress.getByName(teks);
            System.out.printf("%-15s -> %s%n", teks, periksaAlamat(alamat));
        }
    }
}

ValidasiHost.java

Unduh berkas
java
import java.net.URI;
import java.util.Locale;
import java.util.Set;

public class ValidasiHost {
    // Allowlist: satu-satunya tujuan yang boleh dihubungi aplikasi
    static final Set<String> HOST_DIIZINKAN = Set.of("api.example.com", "cdn.example.com");

    static String periksa(String teksUrl) {
        URI uri;
        try {
            uri = URI.create(teksUrl);
        } catch (IllegalArgumentException e) {
            return "DITOLAK (URI tidak valid)";
        }
        if (!"https".equalsIgnoreCase(uri.getScheme())) {
            return "DITOLAK (scheme bukan https)";
        }
        String host = uri.getHost();
        if (host == null) {
            return "DITOLAK (host kosong)";
        }
        if (!HOST_DIIZINKAN.contains(host.toLowerCase(Locale.ROOT))) {
            return "DITOLAK (host tidak ada di allowlist)";
        }
        return "DIIZINKAN";
    }

    public static void main(String[] args) {
        String[] kandidat = {
                "https://api.example.com/v1/produk",
                "https://CDN.example.com/gambar/1.png",
                "http://api.example.com/v1/produk",
                "https://penyerang.example.net/v1/produk",
                "https://[email protected]/v1/produk",
                "https://192.168.1.10/admin",
                "file:///etc/hosts",
        };
        for (String url : kandidat) {
            System.out.printf("%-56s -> %s%n", url, periksa(url));
        }
    }
}