flawopen.com/sql-injection/Java
SQL Injection Java: JDBC, JPA, MyBatis
SQL Injection Java terjadi saat input digabung ke teks kueri. Ikat nilai dengan parameter dan gunakan allowlist untuk identifier seperti ORDER BY.[1], [2], [3], [7], [4], [6]
Bayangkan pabrik yang mengunci cetakan logam sebelum menerima label pelanggan. JDBC PreparedStatement membuat cetakan dengan slot ? terlebih dahulu; setLong(1, userId) kemudian menaruh nilai ke slot tanpa mengubah instruksi. Teks yang digabungkan sebelum persiapan akan terukir pada cetakan. JdbcTemplate dan JPQL setParameter juga memisahkan nilai. MyBatis memperlihatkan perbedaannya: #{status} adalah nilai terikat, sedangkan ${status} adalah teks SQL yang disisipkan. Kolom ORDER BY adalah identifier, bukan nilai; petakan melalui allowlist atau properti Sort.by yang disetujui.[2], [3], [7], [4], [8]
Konsep Kunci & Istilah
PreparedStatement- Perintah JDBC yang dikompilasi dari SQL bertanda
?sebelum nilai apa pun diberikan. Nilai masuk lewat setter bertipe sepertisetLong(1, id)dan tidak pernah bisa mengubah kueri.prepareStatement("..." + id)justru mengompilasi teks penyerang dan tidak melindungi apa pun.[2], [1] JdbcTemplate query(sql, args)queryForList(sql, Long.class, status)danupdate(sql, args...)milik Spring mem-bind argumen di belakang ke penanda?lewat PreparedStatement.NamedParameterJdbcTemplatemelakukan hal yang sama dengan penanda:name. Menggabungkan string ke argumensqlmelewati semua itu.[3], [2]JPQL / HQL injection- Kueri JPA dan Hibernate juga diurai dari string.
createQuery("... = '" + email + "'")membiarkan input mengubah JPQL, yang kemudian diterjemahkan Hibernate menjadi SQL. Gunakan:emaildengansetParameter, atau Criteria API.[7], [1] MyBatis #{} vs ${}- Di mapper MyBatis,
#{status}menjadi parameter?yang di-bind.${status}ditempelkan ke SQL sebagai teks.${}hanya aman untuk nilai yang Anda pilih sendiri, misalnya kolom dari allowlist.[4] Spring Data Sort- Spring Data mengubah
Sort.by("email")menjadi ORDER BY atas properti yang dipetakan dan menolak nama yang tidak dikenal, jadi utamakan ini daripada menyusun ORDER BY secara manual. Tetap petakan kunci request lewat allowlist agar pengguna tidak bisa mengurutkan berdasarkan field yang tidak ingin Anda tampilkan.[8]
Pemetaan kerangka kerja
- Referensi CWE untuk kelemahan yang dijelaskan dalam panduan ini: CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
Alur Serangan Langkah demi Langkah
Nilai request sampai ke repository
Handler Spring @GetMapping menerima @RequestParam String id dan meneruskannya ke findEmail(). Karena tipe yang dideklarasikan adalah String, Spring tidak mengonversi nilai ke tipe lain; validasi formatnya secara eksplisit sebelum mengandalkannya.[9]
Kode Sumber: Rentan vs Aman
// UserRepository.java: setiap kueri di bawah menggabungkan data request ke teks kueri
public class UserRepository {
private final DataSource dataSource;
private final JdbcTemplate jdbc;
private final EntityManager em;
public UserRepository(DataSource dataSource, JdbcTemplate jdbc, EntityManager em) {
this.dataSource = dataSource;
this.jdbc = jdbc;
this.em = em;
}
// JDBC: Statement mengurai string gabungan beserta nilainya
public String findEmail(String userId) throws SQLException {
try (Connection conn = dataSource.getConnection();
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT email FROM users WHERE id = " + userId)) {
return rs.next() ? rs.getString(1) : null;
}
}
// JdbcTemplate: penggabungan terjadi sebelum Spring melihat SQL-nya
public List<Long> findOrderIds(String status) {
return jdbc.queryForList("SELECT id FROM orders WHERE status = '" + status + "'", Long.class);
}
// JPA: JPQL-nya sendiri disusun dari input, lalu diterjemahkan ke SQL
public List<User> findByEmail(String email) {
return em.createQuery("SELECT u FROM User u WHERE u.email = '" + email + "'", User.class)
.getResultList();
}
// field ORDER BY diambil langsung dari request
public List<User> listUsers(String sortField) {
return em.createQuery("SELECT u FROM User u ORDER BY u." + sortField, User.class)
.getResultList();
}
}
// UserRepository.java: nilai berupa parameter bind, field pengurutan berasal dari allowlist
public class UserRepository {
private static final Map<String, String> SORTED_QUERIES = Map.of(
"id", "SELECT u FROM User u ORDER BY u.id",
"email", "SELECT u FROM User u ORDER BY u.email",
"created", "SELECT u FROM User u ORDER BY u.createdAt");
private final DataSource dataSource;
private final JdbcTemplate jdbc;
private final EntityManager em;
public UserRepository(DataSource dataSource, JdbcTemplate jdbc, EntityManager em) {
this.dataSource = dataSource;
this.jdbc = jdbc;
this.em = em;
}
// JDBC: penanda ? dikompilasi dulu, nilai diisi lewat setter bertipe
public String findEmail(long userId) throws SQLException {
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT email FROM users WHERE id = ?")) {
ps.setLong(1, userId);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getString(1) : null;
}
}
}
// JdbcTemplate: argumen di belakang di-bind ke penanda ?
public List<Long> findOrderIds(String status) {
return jdbc.queryForList("SELECT id FROM orders WHERE status = ?", Long.class, status);
}
// JPA: parameter bernama, nilai di-bind dengan setParameter
public List<User> findByEmail(String email) {
return em.createQuery("SELECT u FROM User u WHERE u.email = :email", User.class)
.setParameter("email", email)
.getResultList();
}
// cek allowlist; hanya potongan JPQL konstan yang masuk ke kueri
public List<User> listUsers(String sortKey) {
String jpql = SORTED_QUERIES.getOrDefault(sortKey, SORTED_QUERIES.get("id"));
return em.createQuery(jpql, User.class)
.getResultList();
}
}
Daftar Periksa Penguatan Sistem Rekayasa
- ✓Gagalkan build pada SpotBugs
SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTEserta aturan Find Security BugsSQL_INJECTION_JDBC,SQL_INJECTION_SPRING_JDBC, danSQL_INJECTION_JPA.[10], [5] - ✓Tolak setiap
Statement.execute*,createQuery,createNativeQuery, atau pemanggilan JdbcTemplate yang argumen SQL-nya disusun dengan+,String.format, atauStringBuilder.[2], [3], [7] - ✓Cari
${di mapper MyBatis dalam CI; setiap temuan harus berupa nilai dari allowlist yang ditulis tetap di kode dan didokumentasikan di mapper.[4] - ✓Susun ORDER BY dengan
Sort.by()atau allowlistMapberisi nama properti — jangan pernah langsung dari@RequestParam.[8], [7] - ✓Deklarasikan parameter permintaan dengan tipe domain sebenarnya (
longuntuk ID numerik atauUUIDuntuk ID UUID), agar Spring dapat menolak kegagalan konversi sebelum logika repositori berjalan. Pengikatan parameter SQL tetap menjadi kontrol untuk mencegah SQL Injection.[9], [2]
Sumber
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command
- Oracle — Using Prepared Statements (JDBC tutorial)
- Spring Framework — JdbcTemplate
- MyBatis — Mapper XML: String Substitution
- Find Security Bugs — SQL_INJECTION rules
- OWASP SQL Injection Prevention Cheat Sheet
- Jakarta EE Tutorial — The Jakarta Persistence Query Language
- Spring Data JPA — Query Methods and Sorting
- Spring Framework — @RequestParam
- SpotBugs — SQL Injection Detectors