flawopen.com/sql-injection/Java
SQL Injection em Java: JDBC, JPA e MyBatis
SQL Injection em Java surge ao concatenar entrada na consulta. Use parâmetros vinculados e allowlists para identificadores, como campos de ORDER BY.[1], [2], [3], [7], [4], [6]
Imagine uma fábrica que trava um molde metálico antes de receber as etiquetas dos clientes. JDBC PreparedStatement cria primeiro esse molde com um encaixe ?, e setLong(1, userId) coloca o valor no encaixe sem alterar as instruções. Concatenar texto antes da preparação grava esse texto no molde. JdbcTemplate e JPQL setParameter também mantêm os valores separados. No MyBatis, a diferença fica visível: #{status} é um valor vinculado; ${status} é texto SQL inserido. Campos de ORDER BY são identificadores, não valores: mapeie-os por uma allowlist ou por uma propriedade Sort.by aprovada.[2], [3], [7], [4], [8]
Conceitos Centrais e Termos
PreparedStatement- Um comando JDBC compilado a partir de SQL com marcadores
?antes de qualquer valor ser informado. Os valores entram por setters tipados comosetLong(1, id)e nunca alteram a consulta. UsarprepareStatement("..." + id)compila o texto do atacante e não protege nada.[2], [1] JdbcTemplate query(sql, args)queryForList(sql, Long.class, status)eupdate(sql, args...)do Spring vinculam os argumentos finais aos marcadores?por meio de um PreparedStatement.NamedParameterJdbcTemplatefaz o mesmo com marcadores:name. Concatenar no argumentosqlcontorna tudo isso.[3], [2]JPQL / HQL injection- Consultas JPA e Hibernate também são analisadas a partir de strings.
createQuery("... = '" + email + "'")deixa a entrada alterar o JPQL, que o Hibernate depois traduz para SQL. Use:emailcomsetParameterou a Criteria API.[7], [1] MyBatis #{} vs ${}- Num mapper MyBatis,
#{status}vira um parâmetro?vinculado.${status}é colado no SQL como texto.${}só é seguro para valores escolhidos por você, como uma coluna de uma allowlist.[4] Spring Data Sort- O Spring Data transforma
Sort.by("email")em ORDER BY sobre propriedades mapeadas e rejeita nomes desconhecidos, então prefira-o a montar o ORDER BY à mão. Mesmo assim, mapeie as chaves da requisição por uma allowlist para que o usuário não ordene por campos que você não queria expor.[8]
Mapeamentos de frameworks
- Referência CWE para a fraqueza descrita neste guia: CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
Fluxo de Ataque Passo a Paso
O valor da requisição chega ao repositório
Um handler Spring @GetMapping recebe @RequestParam String id e o repassa a findEmail(). Como o tipo declarado é String, o Spring não converte o valor para outro tipo; valide-o explicitamente antes de confiar em seu formato.[9]
Código-Fonte: Vulnerável vs. Seguro
// UserRepository.java: toda consulta abaixo concatena dados da requisição no texto da 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: o Statement analisa a string concatenada, valor incluído
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: a concatenação acontece antes de o Spring ver o SQL
public List<Long> findOrderIds(String status) {
return jdbc.queryForList("SELECT id FROM orders WHERE status = '" + status + "'", Long.class);
}
// JPA: o próprio JPQL é montado com a entrada e depois traduzido para SQL
public List<User> findByEmail(String email) {
return em.createQuery("SELECT u FROM User u WHERE u.email = '" + email + "'", User.class)
.getResultList();
}
// campo do ORDER BY vindo direto da requisição
public List<User> listUsers(String sortField) {
return em.createQuery("SELECT u FROM User u ORDER BY u." + sortField, User.class)
.getResultList();
}
}
// UserRepository.java: valores são parâmetros vinculados, campos de ordenação vêm de uma 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: marcador ? compilado primeiro, valor definido por um 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: os argumentos finais são vinculados aos marcadores ?
public List<Long> findOrderIds(String status) {
return jdbc.queryForList("SELECT id FROM orders WHERE status = ?", Long.class, status);
}
// JPA: parâmetro nomeado, valor vinculado por 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();
}
// consulta à allowlist; só fragmentos JPQL constantes chegam à 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 Verificação de Segurança para Engenharia
- ✓Faça o build falhar com o SpotBugs
SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTEe com osSQL_INJECTION_JDBC,SQL_INJECTION_SPRING_JDBCeSQL_INJECTION_JPAdo Find Security Bugs.[10], [5] - ✓Rejeite qualquer
Statement.execute*,createQuery,createNativeQueryou chamada do JdbcTemplate cujo argumento SQL seja montado com+,String.formatouStringBuilder.[2], [3], [7] - ✓Procure
${nos mappers MyBatis na CI; cada ocorrência deve ser um valor de uma allowlist fixa no código, documentada no mapper.[4] - ✓Monte o ORDER BY com
Sort.by()ou com uma allowlistMapde nomes de propriedade — nunca diretamente de um@RequestParam.[8], [7] - ✓Declare os parâmetros da requisição com o tipo de domínio correto (
longpara IDs numéricos ouUUIDpara IDs UUID), para que o Spring possa rejeitar falhas de conversão antes da lógica do repositório. Mantenha a vinculação de parâmetros SQL como controle contra SQL Injection.[9], [2]
Fontes
- 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