FlawOpen

flawopen.com/sql-injection/Java

● CWE-89 · Critique
Recherche · FlawOpen

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]

💡 Explication en Langage Simple (ELI5)

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 comme setLong(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) et update(sql, args...) de Spring lient les derniers arguments aux marqueurs ? via un PreparedStatement. NamedParameterJdbcTemplate fait de même avec des marqueurs :name. Concaténer dans l'argument sql contourne 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 :email avec setParameter, 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

Déroulement de l'Attaque Étape par Étape

Étape 1

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]

Étape 2

La concaténation construit le SQL

"... WHERE id = " + userId produit une seule chaîne. Avec la valeur de test inoffensive 0 OR 1=1, on obtient WHERE id = 0 OR 1=1.[1], [2], [6]

Étape 3

Le pilote compile le texte de l'attaquant

createStatement().executeQuery(sql) envoie toute la chaîne à l'analyse. Il n'y a aucun paramètre : la base traite OR 1=1 comme une partie de la clause WHERE.[2], [1]

Étape 4

Toutes les lignes reviennent

La condition est vraie pour chaque ligne, la recherche renvoie donc la table. Il en va de même en JPQL : Hibernate traduit fidèlement la requête modifiée en SQL.[7], [1]

Code Source : Vulnérable vs Sécurisé

IMPLÉMENTATION VULNÉRABLE
// 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();
    }
}
PATCH SÉCURISÉ ET ROBUSTE
// 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

Sources