FlawOpen

flawopen.com/sql-injection/Java

● CWE-89 · Crítica
Pesquisa · FlawOpen

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]

💡 Explicação em Linguagem Simples (ELI5)

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 como setLong(1, id) e nunca alteram a consulta. Usar prepareStatement("..." + id) compila o texto do atacante e não protege nada.[2], [1]
JdbcTemplate query(sql, args)
queryForList(sql, Long.class, status) e update(sql, args...) do Spring vinculam os argumentos finais aos marcadores ? por meio de um PreparedStatement. NamedParameterJdbcTemplate faz o mesmo com marcadores :name. Concatenar no argumento sql contorna 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 :email com setParameter ou 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

Fluxo de Ataque Passo a Paso

Passo 1

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]

Passo 2

A concatenação monta o SQL

"... WHERE id = " + userId gera uma única string. Com o valor de teste inofensivo 0 OR 1=1, ela fica WHERE id = 0 OR 1=1.[1], [2], [6]

Passo 3

O driver compila o texto do atacante

createStatement().executeQuery(sql) envia a string inteira para análise. Não há parâmetros, então o banco trata OR 1=1 como parte da cláusula WHERE.[2], [1]

Passo 4

Todas as linhas voltam

A condição é verdadeira para todas as linhas, e a busca devolve a tabela. O mesmo acontece via JPQL: o Hibernate traduz fielmente a consulta alterada para SQL.[7], [1]

Código-Fonte: Vulnerável vs. Seguro

IMPLEMENTAÇÃO VULNERÁVEL
// 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();
    }
}
PATCH SEGURO E ROBUSTO
// 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

Fontes