flawopen.com/sql-injection/Java
SQL Injection en Java: JDBC, JPA y MyBatis
La inyección SQL en Java surge al concatenar entrada en la consulta. Use parámetros y listas permitidas para identificadores, como campos ORDER BY.[1], [2], [3], [7], [4], [6]
Imagina una fábrica que bloquea una plantilla metálica antes de recibir las etiquetas de los clientes. JDBC PreparedStatement crea primero la plantilla con una ranura ?, y setLong(1, userId) coloca el valor en ella sin cambiar las instrucciones. Concatenar texto antes de preparar la consulta lo graba en la plantilla. JdbcTemplate y JPQL setParameter también mantienen separados los valores. MyBatis muestra la diferencia: #{status} es un valor enlazado; ${status} es texto SQL insertado. Los campos de ORDER BY son identificadores, no valores: asígnalos mediante una lista permitida o una propiedad Sort.by aprobada.[2], [3], [7], [4], [8]
Conceptos Clave y Términos
PreparedStatement- Una sentencia JDBC compilada a partir de SQL con marcadores
?antes de recibir ningún valor. Los valores entran con setters tipados comosetLong(1, id)y nunca alteran la consulta. UsarprepareStatement("..." + id)compila el texto del atacante y no protege nada.[2], [1] JdbcTemplate query(sql, args)queryForList(sql, Long.class, status)yupdate(sql, args...)de Spring enlazan los argumentos finales a los marcadores?mediante un PreparedStatement.NamedParameterJdbcTemplatehace lo mismo con marcadores:name. Concatenar en el argumentosqlse salta todo eso.[3], [2]JPQL / HQL injection- Las consultas de JPA y Hibernate también se analizan a partir de cadenas.
createQuery("... = '" + email + "'")deja que la entrada cambie el JPQL, que Hibernate luego traduce a SQL. Usa:emailconsetParametero la Criteria API.[7], [1] MyBatis #{} vs ${}- En un mapper de MyBatis,
#{status}se convierte en un parámetro?enlazado.${status}se pega en el SQL como texto.${}solo es seguro para valores que eliges tú, como una columna de una lista permitida.[4] Spring Data Sort- Spring Data convierte
Sort.by("email")en un ORDER BY sobre propiedades mapeadas y rechaza nombres desconocidos, así que prefiérelo a construir el ORDER BY a mano. Aun así, pasa las claves de la petición por una lista permitida para que nadie ordene por campos que no querías exponer.[8]
Mapeos de marcos
- Referencia CWE para la debilidad descrita en esta guía: CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
Flujo de Ataque Paso a Paso
El valor de la petición llega al repositorio
Un handler de Spring @GetMapping recibe @RequestParam String id y lo pasa a findEmail(). Como el tipo declarado es String, Spring no convierte el valor a otro tipo; valida explícitamente su formato antes de confiar en él.[9]
Código Fuente: Vulnerable vs. Seguro
// UserRepository.java: cada consulta de abajo concatena datos de la petición en el texto de la consulta
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 analiza la cadena concatenada, valor incluido
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 concatenación ocurre antes de que Spring vea el SQL
public List<Long> findOrderIds(String status) {
return jdbc.queryForList("SELECT id FROM orders WHERE status = '" + status + "'", Long.class);
}
// JPA: el propio JPQL se construye con la entrada y luego se traduce a SQL
public List<User> findByEmail(String email) {
return em.createQuery("SELECT u FROM User u WHERE u.email = '" + email + "'", User.class)
.getResultList();
}
// campo de ORDER BY tomado directamente de la petición
public List<User> listUsers(String sortField) {
return em.createQuery("SELECT u FROM User u ORDER BY u." + sortField, User.class)
.getResultList();
}
}
// UserRepository.java: los valores son parámetros enlazados, los campos de orden salen de una lista permitida
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: marcador ? compilado primero, valor asignado con un setter tipado
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: los argumentos finales se enlazan a los marcadores ?
public List<Long> findOrderIds(String status) {
return jdbc.queryForList("SELECT id FROM orders WHERE status = ?", Long.class, status);
}
// JPA: parámetro con nombre, valor enlazado con 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();
}
// búsqueda en la lista permitida; solo fragmentos JPQL constantes llegan a la consulta
public List<User> listUsers(String sortKey) {
String jpql = SORTED_QUERIES.getOrDefault(sortKey, SORTED_QUERIES.get("id"));
return em.createQuery(jpql, User.class)
.getResultList();
}
}
Lista de Verificación de Seguridad para Ingeniería
- ✓Haz que el build falle con SpotBugs
SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTEy conSQL_INJECTION_JDBC,SQL_INJECTION_SPRING_JDBCySQL_INJECTION_JPAde Find Security Bugs.[10], [5] - ✓Rechaza cualquier
Statement.execute*,createQuery,createNativeQueryo llamada de JdbcTemplate cuyo argumento SQL se construya con+,String.formatoStringBuilder.[2], [3], [7] - ✓Busca
${en los mappers de MyBatis en la CI; cada coincidencia debe ser un valor de una lista permitida fija en el código y documentada en el mapper.[4] - ✓Construye el ORDER BY con
Sort.by()o con una lista permitidaMapde nombres de propiedad, nunca directamente desde un@RequestParam.[8], [7] - ✓Declara los parámetros de la petición con su tipo de dominio real (
longpara IDs numéricos oUUIDpara IDs UUID), para que Spring pueda rechazar conversiones fallidas antes de la lógica del repositorio. Mantén la vinculación de parámetros SQL como control contra SQL Injection.[9], [2]
Fuentes
- 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