flawopen.com/sql-injection/Java
Java-SQL-Injection: JDBC, JPA, MyBatis
SQL-Injection in Java entsteht durch verkettete Eingaben im SQL. Nutzen Sie gebundene Parameter und Allowlists für Bezeichner wie ORDER-BY-Felder.[1], [2], [3], [7], [4], [6]
Stell dir eine Fabrik vor, die eine Metall-Schablone verriegelt, bevor Kundenetiketten eintreffen. JDBC PreparedStatement erstellt zuerst die Schablone mit einer ?-Aufnahme; setLong(1, userId) setzt einen Wert ein, ohne die Anweisungen zu ändern. Vor der Vorbereitung verketteter Text wird dagegen in die Schablone graviert. Auch JdbcTemplate und JPQL setParameter halten Werte getrennt. MyBatis zeigt den Unterschied deutlich: #{status} ist ein gebundener Wert, ${status} eingefügter SQL-Text. Spalten für ORDER BY sind Bezeichner, keine Werte: Ordne sie einer Allowlist oder einer freigegebenen Sort.by-Eigenschaft zu.[2], [3], [7], [4], [8]
Kernkonzepte & Begriffe
PreparedStatement- Eine JDBC-Anweisung, die aus SQL mit
?-Markern kompiliert wird, bevor ein Wert vorliegt. Werte kommen über typisierte Setter wiesetLong(1, id)hinein und können die Abfrage nie verändern.prepareStatement("..." + id)kompiliert den Text des Angreifers und schützt nichts.[2], [1] JdbcTemplate query(sql, args)- Springs
queryForList(sql, Long.class, status)undupdate(sql, args...)binden die nachfolgenden Argumente über ein PreparedStatement an die?-Marker.NamedParameterJdbcTemplatemacht dasselbe mit:name-Markern. Wer inssql-Argument verkettet, umgeht all das.[3], [2] JPQL / HQL injection- Auch JPA- und Hibernate-Abfragen werden aus Strings geparst.
createQuery("... = '" + email + "'")lässt Eingaben das JPQL verändern, das Hibernate anschließend in SQL übersetzt. Verwenden Sie:emailmitsetParameteroder die Criteria-API.[7], [1] MyBatis #{} vs ${}- In einem MyBatis-Mapper wird
#{status}zu einem gebundenen?-Parameter.${status}wird als Text ins SQL kopiert.${}ist nur für Werte sicher, die Sie selbst wählen, etwa eine Spalte aus einer Allowlist.[4] Spring Data Sort- Spring Data macht aus
Sort.by("email")ein ORDER BY über gemappte Properties und lehnt unbekannte Namen ab – also lieber das als handgebautes ORDER BY. Request-Schlüssel trotzdem über eine Allowlist führen, damit niemand nach Feldern sortiert, die Sie nicht freigeben wollten.[8]
Framework-Zuordnungen
- CWE-Referenz für die in diesem Leitfaden beschriebene Schwachstelle: CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
Schritt-für-Schritt Angriffsablauf
Der Request-Wert erreicht das Repository
Ein Spring-Handler mit @GetMapping nimmt @RequestParam String id entgegen und reicht es an findEmail() weiter. Da der deklarierte Typ String ist, wandelt Spring den Wert nicht in einen anderen Typ um; sein Format muss vor der Verwendung ausdrücklich geprüft werden.[9]
Quellcode: Verwundbar vs. Sicher
// UserRepository.java: jede Abfrage unten verkettet Request-Daten in den Abfragetext
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 parst den verketteten String samt Wert
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: verkettet wird, bevor Spring das SQL überhaupt sieht
public List<Long> findOrderIds(String status) {
return jdbc.queryForList("SELECT id FROM orders WHERE status = '" + status + "'", Long.class);
}
// JPA: schon das JPQL wird aus Eingaben gebaut und dann in SQL übersetzt
public List<User> findByEmail(String email) {
return em.createQuery("SELECT u FROM User u WHERE u.email = '" + email + "'", User.class)
.getResultList();
}
// ORDER-BY-Feld direkt aus dem Request
public List<User> listUsers(String sortField) {
return em.createQuery("SELECT u FROM User u ORDER BY u." + sortField, User.class)
.getResultList();
}
}
// UserRepository.java: Werte sind gebundene Parameter, Sortierfelder kommen aus einer 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: ?-Marker zuerst kompiliert, Wert per typisiertem Setter gesetzt
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: nachfolgende Argumente werden an die ?-Marker gebunden
public List<Long> findOrderIds(String status) {
return jdbc.queryForList("SELECT id FROM orders WHERE status = ?", Long.class, status);
}
// JPA: benannter Parameter, Wert per setParameter gebunden
public List<User> findByEmail(String email) {
return em.createQuery("SELECT u FROM User u WHERE u.email = :email", User.class)
.setParameter("email", email)
.getResultList();
}
// Allowlist-Abgleich; nur konstante JPQL-Fragmente gelangen in die Abfrage
public List<User> listUsers(String sortKey) {
String jpql = SORTED_QUERIES.getOrDefault(sortKey, SORTED_QUERIES.get("id"));
return em.createQuery(jpql, User.class)
.getResultList();
}
}
Checkliste für Engineering & Systemsicherheit
- ✓Den Build bei SpotBugs
SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTEund den Find-Security-Bugs-RegelnSQL_INJECTION_JDBC,SQL_INJECTION_SPRING_JDBCundSQL_INJECTION_JPAscheitern lassen.[10], [5] - ✓Jedes
Statement.execute*,createQuery,createNativeQueryoder jeden JdbcTemplate-Aufruf ablehnen, dessen SQL-Argument mit+,String.formatoderStringBuildergebaut wird.[2], [3], [7] - ✓MyBatis-Mapper im CI nach
${durchsuchen; jeder Treffer muss ein Wert aus einer fest codierten, im Mapper dokumentierten Allowlist sein.[4] - ✓ORDER BY über
Sort.by()oder eineMap-Allowlist von Property-Namen bauen – nie direkt aus einem@RequestParam.[8], [7] - ✓Request-Parameter mit dem tatsächlichen Domänentyp deklarieren (
longfür numerische IDs oderUUIDfür UUIDs), damit Spring fehlgeschlagene Konvertierungen vor der Repository-Logik zurückweisen kann. SQL-Parameterbindung bleibt die Schutzmaßnahme gegen SQL-Injection.[9], [2]
Quellen
- 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