Setiap pintu yang kita buka sepanjang section ini — pemanggilan fungsi native, pemuatan pustaka eksternal, peluncuran proses anak — memperluas wilayah yang diawasi JVM menjadi wilayah yang tidak lagi diawasinya. Sistem perizinan Java, pembatasan memori, dan pesan exception berhenti tepat di ambang batas tersebut, sehingga keputusan keamanan bergeser ke tangan kode kita. Tiga sumber risiko paling umum: masukan pengguna yang sampai ke shell, rahasia yang ikut terwariskan ke proses anak, dan path pustaka native yang disusun dari data luar. Chapter ini membahas ketiganya dengan pola contoh rentan diikuti versi aman, ditutup mekanisme izin akses native milik FFM.
Injeksi Perintah pada Proses Anak #
Pola rentannya muncul ketika masukan pengguna dirangkai menjadi satu string perintah untuk sh -c: tanda titik koma pada masukan menjadi pemisah perintah sungguhan bagi shell. Program berikut mensimulasikan masukan jahat dengan nilai tetap yang tidak berbahaya — tanda sisipan Perintah-Tersisip membuktikan ekstra perintah itu benar-benar dieksekusi:
import java.nio.charset.StandardCharsets;
public class InjeksiRentan {
public static void main(String[] args) throws Exception {
// Masukan pengguna disimulasikan dengan nilai tetap yang berbahaya.
String berkasPilihan = "laporan.txt; echo Perintah-Tersisip";
// POLA RENTAN: masukan dirangkai ke satu string perintah shell.
String perintah = "wc -c " + berkasPilihan;
Process anak = new ProcessBuilder("/bin/sh", "-c", perintah).start();
String keluaran = new String(anak.getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
anak.waitFor();
System.out.println("perintah : " + perintah);
System.out.println("keluaran :");
System.out.print(keluaran);
}
}Output yang Dihasilkan:
perintah : wc -c laporan.txt; echo Perintah-Tersisip
keluaran :
Perintah-TersisipShell memenggal perintah menjadi dua: wc -c laporan.txt (gagal karena berkas tidak ada) lalu echo Perintah-Tersisip yang berjalan sukses — keduanya terjadi hanya karena satu titik koma lolos. Pada sistem nyata, pola serupa bisa menjalankan pembacaan berkas rahasia atau penambahan kunci SSH. Versi amannya menghapus shell dari persamaan sepenuhnya: masukan menjadi satu argumen utuh dari daftar perintah:
import java.nio.charset.StandardCharsets;
public class InjeksiAman {
public static void main(String[] args) throws Exception {
// Masukan yang sama persis dengan versi rentan.
String berkasPilihan = "laporan.txt; echo Perintah-Tersisip";
// POLA AMAN: tanpa shell; masukan menjadi satu argumen utuh.
ProcessBuilder pb = new ProcessBuilder("/usr/bin/wc", "-c", berkasPilihan);
pb.redirectErrorStream(true);
Process anak = pb.start();
String keluaran = new String(anak.getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
int kode = anak.waitFor();
System.out.println("argumen : [" + berkasPilihan + "]");
System.out.println("exit code: " + kode);
System.out.println("keluaran :");
System.out.print(keluaran);
}
}Output yang Dihasilkan:
argumen : [laporan.txt; echo Perintah-Tersisip]
exit code: 1
keluaran :
wc: 'laporan.txt; echo Perintah-Tersisip': No such file or directoryTanpa shell, string utuh itu diperlakukan sebagai nama berkas — wc menolaknya karena berkasnya memang tidak ada, dan exit code 1 melaporkan kegagalan secara jujur. Bila nama berkas sah, wc membacanya tanpa satu pun karakter ditafsirkan.
Rahasia pada Environment dan Path Pustaka #
Dua kebocoran berikutnya lebih senyap karena tidak menghasilkan pesan galat. Pertama, environment anak adalah salinan environment parent: variabel token yang kita set untuk keperluan JVM ikut terbawa ke setiap proses anak. Pola amannya membersihkan variabel berbau rahasia sebelum peluncuran:
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class EnvBersih {
public static void main(String[] args) throws Exception {
ProcessBuilder pb = new ProcessBuilder(
"/bin/sh", "-c", "echo TOKEN=[$DATABASE_TOKEN]");
Map<String, String> env = pb.environment();
// Simulasi rahasia yang warisan dari environment parent.
env.put("DATABASE_TOKEN", "YOUR_API_KEY");
// Pola aman: buang variabel yang berbau rahasia sebelum meluncurkan.
env.keySet().removeIf(k -> k.contains("TOKEN")
|| k.contains("KEY") || k.contains("SECRET"));
Process anak = pb.start();
String keluaran = new String(anak.getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
anak.waitFor();
System.out.println("anak melihat " + keluaran.trim());
}
}Output yang Dihasilkan:
anak melihat TOKEN=[]Prinsip yang lebih ketat lagi: jangan mewariskan seluruh environment, melainkan bangun environment minimal dari nol — hanya variabel yang benar-benar dibutuhkan anak.
Kedua, path pustaka native yang berasal dari data luar membuka peluang path traversal — ../ bisa meloloskan pemuatan ke berkas di luar direktori yang diizinkan. Pertahanannya: normalisasi path lalu cocokkan dengan basis direktori:
import java.nio.file.Path;
public class ValidasiPath {
public static void main(String[] args) {
// Basis pustaka: satu-satunya direktori yang boleh memuat native.
Path basis = Path.of("pustaka").toAbsolutePath().normalize();
String[] kandidat = {
"pustaka/libaudio.so", // sah
"pustaka/../../etc/passwd", // percobaan keluar basis
"/etc/passwd" // path absolut mentah
};
for (String k : kandidat) {
Path p = Path.of(k).toAbsolutePath().normalize();
boolean sah = p.startsWith(basis);
System.out.printf("%-28s sah=%b%n", k, sah);
}
}
}Output yang Dihasilkan:
pustaka/libaudio.so sah=true
pustaka/../../etc/passwd sah=false
/etc/passwd sah=falseNormalisasi melelehkan ../ sebelum pemeriksaan, dan startsWith(basis) pada path absolut memastikan hasil akhirnya tetap di dalam basis — pola yang juga berlaku untuk validasi berkas unggahan maupun path hasil konfigurasi.
Izin Akses Native pada FFM #
FFM sendiri datang dengan sistem izin. Method seperti Linker.nativeLinker() dan SymbolLookup.libraryLookup() tergolong restricted: tanpa izin eksplisit, JVM mencetak peringatan ke stderr pada pemanggilan pertama — baris berikut dikutip dari eksekusi nyata tanpa flag:
WARNING: A restricted method in java.lang.foreign.Linker has been called
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabledDua cara memberi izin: flag --enable-native-access=ALL-UNNAMED saat menjalankan aplikasi tanpa nama module, atau atribut Enable-Native-Access pada manifest modul bernama — pendekatan kedua lebih presisi karena izin melekat pada modul, bukan pada seluruh JVM. Arah ke depannya jelas: pemanggilan tanpa izin akan diblokir, jadi sebaiknya flag dipasang sejak sekarang.
Section Interoperability dan Native Integration ditutup di sini: kita bisa keluar dari JVM lewat FFM maupun proses anak tanpa kehilangan kendali. Section berikutnya berpindah peran — dari memanggil sistem operasi menjadi menyelami produktivitas Java itu sendiri, dimulai dari jshell sebagai ruang eksperimen harian.
Referensi #
- Oracle: Secure Coding Guidelines for Java
- Oracle: ProcessBuilder API
- OpenJDK: JEP 454 — Foreign Function & Memory API
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
EnvBersih.java
Unduh berkasimport java.nio.charset.StandardCharsets;
import java.util.Map;
public class EnvBersih {
public static void main(String[] args) throws Exception {
ProcessBuilder pb = new ProcessBuilder(
"/bin/sh", "-c", "echo TOKEN=[$DATABASE_TOKEN]");
Map<String, String> env = pb.environment();
// Simulasi rahasia yang warisan dari environment parent.
env.put("DATABASE_TOKEN", "YOUR_API_KEY");
// Pola aman: buang variabel yang berbau rahasia sebelum meluncurkan.
env.keySet().removeIf(k -> k.contains("TOKEN")
|| k.contains("KEY") || k.contains("SECRET"));
Process anak = pb.start();
String keluaran = new String(anak.getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
anak.waitFor();
System.out.println("anak melihat " + keluaran.trim());
}
}
InjeksiAman.java
Unduh berkasimport java.nio.charset.StandardCharsets;
public class InjeksiAman {
public static void main(String[] args) throws Exception {
// Masukan yang sama persis dengan versi rentan.
String berkasPilihan = "laporan.txt; echo Perintah-Tersisip";
// POLA AMAN: tanpa shell; masukan menjadi satu argumen utuh.
ProcessBuilder pb = new ProcessBuilder("/usr/bin/wc", "-c", berkasPilihan);
pb.redirectErrorStream(true);
Process anak = pb.start();
String keluaran = new String(anak.getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
int kode = anak.waitFor();
System.out.println("argumen : [" + berkasPilihan + "]");
System.out.println("exit code: " + kode);
System.out.println("keluaran :");
System.out.print(keluaran);
}
}
InjeksiRentan.java
Unduh berkasimport java.nio.charset.StandardCharsets;
public class InjeksiRentan {
public static void main(String[] args) throws Exception {
// Masukan pengguna disimulasikan dengan nilai tetap yang berbahaya.
String berkasPilihan = "laporan.txt; echo Perintah-Tersisip";
// POLA RENTAN: masukan dirangkai ke satu string perintah shell.
String perintah = "wc -c " + berkasPilihan;
Process anak = new ProcessBuilder("/bin/sh", "-c", perintah).start();
String keluaran = new String(anak.getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
anak.waitFor();
System.out.println("perintah : " + perintah);
System.out.println("keluaran :");
System.out.print(keluaran);
}
}
ValidasiPath.java
Unduh berkasimport java.nio.file.Path;
public class ValidasiPath {
public static void main(String[] args) {
// Basis pustaka: satu-satunya direktori yang boleh memuat native.
Path basis = Path.of("pustaka").toAbsolutePath().normalize();
String[] kandidat = {
"pustaka/libaudio.so", // sah
"pustaka/../../etc/passwd", // percobaan keluar basis
"/etc/passwd" // path absolut mentah
};
for (String k : kandidat) {
Path p = Path.of(k).toAbsolutePath().normalize();
boolean sah = p.startsWith(basis);
System.out.printf("%-28s sah=%b%n", k, sah);
}
}
}