Java menangani banyak hal dengan aman secara bawaan, mulai dari manajemen memori sampai pemeriksaan batas array, tetapi aman di tingkat bahasa tidak otomatis berarti aplikasinya aman. Tiga wilayah risiko yang paling sering menimpa proyek Java adalah injection, deserialisasi data yang tidak tepercaya, dan rantai dependensi yang dibawa masuk lewat alat build. Topik ini mengenalkan ketiganya dengan demo yang tetap dapat dijalankan, karena risiko semacam ini paling mudah dipahami ketika bentuknya terlihat nyata.
Injection: masukan yang berubah menjadi perintah#
Injection terjadi ketika data dari luar digabungkan ke dalam teks yang lalu ditafsirkan sebagai perintah, sehingga bagian data berubah menjadi bagian perintah. Bentuk yang paling terkenal adalah SQL injection, yaitu masukan pengguna yang digabung langsung ke teks kuery. Program berikut tidak benar-benar mengirim kuery ke database, tetapi mencetak teks kuery persis seperti yang akan diterima server, sehingga mekanismenya tetap terlihat utuh.
public class DemoInjeksiKuery {
public static void main(String[] args) {
String masukan = "kancil' OR '1'='1";
// rapuh: masukan digabung langsung ke teks kuery
String rapuh = String.format("SELECT * FROM buku WHERE judul = '%s'", masukan);
System.out.println("kuery rapuh : " + rapuh);
// aman: masukan tidak pernah menjadi bagian teks kuerynya
String aman = "SELECT * FROM buku WHERE judul = ?";
System.out.println("kuery aman : " + aman);
System.out.println("nilai ikatan: " + masukan);
}
}kuery rapuh : SELECT * FROM buku WHERE judul = 'kancil' OR '1'='1'
kuery aman : SELECT * FROM buku WHERE judul = ?
nilai ikatan: kancil' OR '1'='1Pada kuery rapuh, kutip tunggal yang diselipkan masukan menutup kutip pembuka lebih awal, dan sisanya menjadi logika kuery baru, yaitu syarat OR '1'='1' yang selalu benar sehingga seluruh isi tabel terbaca. Pada kuery aman, teks kuerynya tetap dan masukan hanya mengisi satu tempat parameter, sehingga isinya tidak pernah ditafsirkan sebagai logika. Perlindungannya sudah diperkenalkan pada topik JDBC berupa PreparedStatement, dan aturan tunggalnya adalah kuery dengan bagian yang tetap ditulis sebagai teks, sedangkan semua nilai dibawa sebagai parameter.
Bentuk injection lain menyerang shell, dan cara melihatnya sama persis, yaitu membaca teks perintah yang disusun dari masukan.
import java.util.List;
public class DemoInjeksiPerintah {
public static void main(String[] args) {
String namaBerkas = "laporan.pdf; rm -rf /home/ali/arsip";
// rapuh: perintah disusun sebagai satu string untuk shell
System.out.println("perintah rapuh : pdfinfo " + namaBerkas);
// aman: nama berkas dioper sebagai satu argumen terpisah
List<String> argumen = List.of("pdfinfo", namaBerkas);
System.out.println("argumen aman : " + argumen);
}
}perintah rapuh : pdfinfo laporan.pdf; rm -rf /home/ali/arsip
argumen aman : [pdfinfo, laporan.pdf; rm -rf /home/ali/arsip]Dalam bentuk rapuh, tanda titik koma yang diselipkan dianggap pemisah perintah oleh shell, sehingga perintah kedua ikut dieksekusi. Dalam bentuk aman, ProcessBuilder menerima daftar argumen terpisah, sehingga string berbahaya itu hanyalah satu nilai argumen dan tidak pernah berubah menjadi pemisah perintah. Polanya identik dengan SQL, yaitu struktur dan data tidak boleh disatukan dalam satu teks.
Deserialisasi data yang tidak tepercaya#
Deserialisasi Java membangun ulang objek dari aliran byte, dan mekanisme itu menyimpan risiko yang mudah diabaikan. Saat ObjectInputStream.readObject() dipanggil pada data dari luar, kode konstruksi dari kelas mana pun yang tersedia di classpath dapat ikut berjalan, dan pemanggil tidak memilih kelas apa yang hidup. Penyerang yang menguasai byte tersebut dapat merangkai urutan panggilan yang disebut gadget chain untuk menjalankan aksi berbahaya, dan risiko ini sudah diperkenalkan sebagai peringatan pada topik Data Biner dan Serialisasi.
// tidak-dijalankan
// ObjectInputStream membangun ulang objek dari byte, apa pun kelasnya
try (ObjectInputStream masuk =
new ObjectInputStream(new FileInputStream(berkasDariLuar))) {
Object objek = masuk.readObject();
System.out.println("objek hidup kembali: " + objek);
}Pertahanan utamanya bersifat kebijakan, yaitu deserialisasi Java hanya boleh dilakukan pada data yang berasal dari pihak tepercaya, dan data dari pengguna atau layanan luar tidak termasuk di dalamnya. Untuk pertukaran data dengan pihak luar, format berteks yang disusuri skema seperti JSON jauh lebih aman, karena parsernya membuat nilai, bukan objek dengan perilaku. Bila format biner diperlukan, pilih format data murni tanpa kode yang ikut hidup, dan batasi kelas yang boleh dibangun ulang bila ObjectInputStream tetap tidak bisa dihindari.
Risiko pada rantai dependensi#
Proyek Java jarang hanya memakai JDK, dan setiap dependensi di POM membawa dependensi transitifnya sendiri, sehingga keamanan proyek ikut ditentukan oleh kode yang bahkan tidak pernah dibaca. Ada dua risiko utama yang berjalan beriringan. Pertama, versi lama yang punya celah keamanan yang sudah diketahui publik, biasanya tercatat sebagai CVE. Kedua, dependensi yang tidak lagi dirawat, sehingga celahnya tidak akan pernah ditambal.
# tidak-dijalankan
# memeriksa dependensi proyek terhadap basis celah keamanan publik
mvn org.owasp:dependency-check-maven:check
# melihat seluruh pohon dependensi, termasuk versi yang terbawa transitif
mvn dependency:treeAlat seperti OWASP Dependency-Check membandingkan daftar dependensi proyek dengan basis celah yang diketahui, lalu melaporkan versi yang bermasalah beserta tingkat keparahannya. Layanan hosting repository juga memperingatkan versi yang terinfeksi, dan kebiasaan memeriksa peringatan tersebut saat menambah dependensi baru sangat murah dibanding biaya kejadian. Prinsip penutupnya sederhana, yaitu dependensi adalah kode yang ikut berjalan, sehingga jumlahnya minimalkan dan versinya segar.
Kebiasaan dasar yang murah dan efektif#
Sebelum menutup topik, sekelompok kebiasaan dasar layak disebut karena biayanya kecil dan dampaknya menutup banyak celah sekaligus. Validasi masukan di tepi program menjaga data aneh tidak masuk ke lapisan dalam, termasuk menyepakati panjang dan format sebelum teks diproses. Menjalankan program dengan hak akses terkecil membatasi kerusakan bila terobos, sehingga layanan tidak berjalan dengan hak administrator tanpa alasan.
Kebiasaan berikutnya bergerak di sisi perawatan. Versi JDK dan dependensi diperbarui secara teratur, karena versi baru biasanya membawa tambalan celah yang sudah diketahui. Nilai rahasia seperti sandi dan API key tidak pernah ditulis di kode atau log, melainkan disimpan di konfigurasi luar yang tidak ikut masuk repository. Terakhir, error yang ditangani jangan mencetak detail internal ke pengguna luar, karena pesan exception yang mentah sering membocorkan struktur program bagi penyerang.
Kesalahan umum#
- Menyusun kuery dengan penggabungan string karena datanya dianggap tepercaya. Sumber data berubah lebih cepat daripada asumsi, dan satu jalur masukan baru cukup untuk membuka celahnya. Gunakan parameterized query tanpa kecuali.
- Mematikan validasi atau enkripsi sementara untuk mempercepat pengujian. Konfigurasi sementara seperti itu sering terbawa sampai produksi. Buat lingkungan uji yang tetap memakai jalur keamanan penuh.
- Menganggap framework menangani semua masalah keamanan. Framework mengurangi risiko pada jalurnya, sedangkan keputusan seperti bentuk kuery, pembersihan berkas, dan penyimpanan rahasia tetap milik kode sendiri.
- Memasang dependensi tanpa memeriksa umurnya. Library yang terakhir disunting bertahun-tahun lalu adalah utang keamanan yang ditunda. Periksa pemeliharaannya sebelum ikut terbawa, dan catat versinya di POM secara eksplisit.
- Mengira field
privatemelindungi data yang diserialisasi. Java Serialization menulis dan membangun ulang objek lewat jalur khusus yang tidak menyentuh accessor biasa. Keamanan data terletak pada siapa yang boleh mengirim byte, bukan pada modifier field.
Ringkasan#
- Injection terjadi ketika data luar menjadi bagian perintah, dan pencegahannya adalah memisahkan struktur dari data, baik lewat
PreparedStatementmaupun daftar argumenProcessBuilder. - Deserialisasi
ObjectInputStreampada data tidak tepercaya dapat menjalankan kode dari kelas mana pun di classpath, sehingga data luar diterima lewat format data murni berteks. - Rantai dependensi membawa risiko versi lama dan pustaka tak terawat, dan alat seperti OWASP Dependency-Check memeriksanya terhadap basis celah publik.
- Validasi di tepi, hak akses terkecil, pembaruan rutin, dan menjauhkan rahasia dari kode adalah kebiasaan murah yang menutup banyak celah sekaligus.
Referensi#
- OWASP Top 10 — OWASP Foundation — daftar risiko keamanan aplikasi web yang paling kritis dan sering ditemui.
- SQL Injection Prevention Cheat Sheet — OWASP — panduan praktis mencegah SQL injection dengan parameterized query.
- Secure Coding Guidelines for Java SE — Oracle — pedoman penulisan kode Java yang aman dari Oracle.
- OWASP Dependency-Check — OWASP Foundation — alat pemeriksaan dependensi terhadap basis celah keamanan yang diketahui.