flawopen.com/sql-injection/Java
Injection SQL en Java : JDBC, JPA, MyBatis
Liez les valeurs ; n’acceptez les identifiants ORDER BY que depuis une liste d’autorisation fermée.[1], [2], [3], [7], [4], [6]
Imaginez une usine qui verrouille un gabarit métallique avant l’arrivée des étiquettes des clients. JDBC PreparedStatement crée d’abord le gabarit avec un emplacement ?, puis setLong(1, userId) place une valeur dans cet emplacement sans modifier les instructions. Une concaténation avant la préparation grave le texte dans le gabarit. JdbcTemplate et JPQL setParameter séparent aussi les valeurs. MyBatis rend la différence visible : #{status} est une valeur liée, tandis que ${status} est du texte SQL inséré. Les champs ORDER BY sont des identifiants, pas des valeurs : associez-les à une liste autorisée ou à une propriété Sort.by approuvée.[2], [3], [7], [4], [8]
Concepts Clés et Termes
PreparedStatement- Une instruction JDBC compilée à partir d'un SQL à marqueurs
?avant qu'aucune valeur ne soit fournie. Les valeurs passent par des setters typés commesetLong(1, id)et ne peuvent jamais modifier la requête.prepareStatement("..." + id)compile le texte de l'attaquant et ne protège rien.[2], [1] JdbcTemplate query(sql, args)queryForList(sql, Long.class, status)etupdate(sql, args...)de Spring lient les derniers arguments aux marqueurs?via un PreparedStatement.NamedParameterJdbcTemplatefait de même avec des marqueurs:name. Concaténer dans l'argumentsqlcontourne tout cela.[3], [2]JPQL / HQL injection- Les requêtes JPA et Hibernate sont elles aussi analysées depuis des chaînes.
createQuery("... = '" + email + "'")laisse la saisie modifier le JPQL, que Hibernate traduit ensuite en SQL. Utilisez:emailavecsetParameter, ou l'API Criteria.[7], [1] MyBatis #{} vs ${}- Dans un mapper MyBatis,
#{status}devient un paramètre?lié.${status}est collé tel quel dans le SQL.${}n'est sûr que pour des valeurs choisies par vous, comme une colonne issue d'une liste autorisée.[4] Spring Data Sort- Spring Data transforme
Sort.by("email")en ORDER BY sur des propriétés mappées et rejette les noms inconnus : préférez-le à un ORDER BY écrit à la main. Faites tout de même passer les clés de requête par une liste autorisée pour qu'on ne trie pas sur des champs que vous ne vouliez pas exposer.[8]
Correspondances de référentiels
- Référence CWE pour la faiblesse décrite dans ce guide : CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
Déroulement de l'Attaque Étape par Étape
La valeur de la requête atteint le repository
Un handler Spring @GetMapping reçoit @RequestParam String id et le transmet à findEmail(). Comme le type déclaré est String, Spring ne convertit pas la valeur vers un autre type ; validez explicitement son format avant de vous y fier.[9]
Code Source : Vulnérable vs Sécurisé
// UserRepository.java : chaque requête ci-dessous concatène des données HTTP dans son texte
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 analyse la chaîne concaténée, valeur comprise
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 : la concaténation a lieu avant que Spring ne voie le SQL
public List<Long> findOrderIds(String status) {
return jdbc.queryForList("SELECT id FROM orders WHERE status = '" + status + "'", Long.class);
}
// JPA : le JPQL lui-même est construit avec la saisie, puis traduit en SQL
public List<User> findByEmail(String email) {
return em.createQuery("SELECT u FROM User u WHERE u.email = '" + email + "'", User.class)
.getResultList();
}
// champ d'ORDER BY tiré directement de la requête
public List<User> listUsers(String sortField) {
return em.createQuery("SELECT u FROM User u ORDER BY u." + sortField, User.class)
.getResultList();
}
}
// UserRepository.java : les valeurs sont des paramètres liés, les champs de tri viennent d'une liste autorisée
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 : marqueur ? compilé d'abord, valeur fixée par un setter typé
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 : les derniers arguments sont liés aux marqueurs ?
public List<Long> findOrderIds(String status) {
return jdbc.queryForList("SELECT id FROM orders WHERE status = ?", Long.class, status);
}
// JPA : paramètre nommé, valeur liée par 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();
}
// recherche dans la liste autorisée ; seuls des fragments JPQL constants atteignent la requête
public List<User> listUsers(String sortKey) {
String jpql = SORTED_QUERIES.getOrDefault(sortKey, SORTED_QUERIES.get("id"));
return em.createQuery(jpql, User.class)
.getResultList();
}
}
Liste de Contrôle de Sécurité pour l'Ingénierie
- ✓Faites échouer le build sur SpotBugs
SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTEet surSQL_INJECTION_JDBC,SQL_INJECTION_SPRING_JDBCetSQL_INJECTION_JPAde Find Security Bugs.[10], [5] - ✓Refusez tout
Statement.execute*,createQuery,createNativeQueryou appel JdbcTemplate dont l'argument SQL est construit avec+,String.formatouStringBuilder.[2], [3], [7] - ✓Recherchez
${dans les mappers MyBatis en CI ; chaque occurrence doit être une valeur d'une liste autorisée codée en dur et documentée dans le mapper.[4] - ✓Construisez l'ORDER BY avec
Sort.by()ou une liste autoriséeMapde noms de propriétés — jamais directement depuis un@RequestParam.[8], [7] - ✓Déclarez les paramètres de requête avec leur type métier réel (
longpour les identifiants numériques ouUUIDpour les identifiants UUID), afin que Spring puisse rejeter les échecs de conversion avant la logique du dépôt. La liaison des paramètres SQL reste la protection contre les injections SQL.[9], [2]
Sources
- 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