flawopen.com/sql-injection/Java
SQL-инъекция в Java: JDBC, JPA, MyBatis
SQL-инъекция в Java возникает при конкатенации ввода в запрос. Используйте параметры для значений и белый список для полей ORDER BY.[1], [2], [3], [7], [4], [6]
Представьте фабрику, которая фиксирует металлический трафарет до получения клиентских бирок. JDBC PreparedStatement сначала создаёт трафарет с гнездом ?, а setLong(1, userId) помещает значение в гнездо, не меняя инструкции. Конкатенация до подготовки запроса гравирует текст прямо в трафарете. JdbcTemplate и JPQL setParameter также отделяют значения. MyBatis наглядно показывает разницу: #{status} — связанное значение, а ${status} — вставленный SQL-текст. Поля ORDER BY — идентификаторы, а не значения: сопоставляйте их с allowlist или разрешённым свойством Sort.by.[2], [3], [7], [4], [8]
Ключевые понятия и термины
PreparedStatement- Оператор JDBC, скомпилированный из SQL с маркерами
?до того, как переданы какие-либо значения. Значения поступают через типизированные сеттеры вродеsetLong(1, id)и не могут изменить запрос.prepareStatement("..." + id)компилирует текст атакующего и ничего не защищает.[2], [1] JdbcTemplate query(sql, args)queryForList(sql, Long.class, status)иupdate(sql, args...)в Spring привязывают последние аргументы к маркерам?через PreparedStatement.NamedParameterJdbcTemplateделает то же с маркерами:name. Конкатенация в аргументsqlобходит всё это.[3], [2]JPQL / HQL injection- Запросы JPA и Hibernate тоже разбираются из строк.
createQuery("... = '" + email + "'")позволяет вводу изменить JPQL, который Hibernate затем переводит в SQL. Используйте:emailсsetParameterили Criteria API.[7], [1] MyBatis #{} vs ${}- В маппере MyBatis
#{status}превращается в привязанный параметр?.${status}вставляется в SQL как текст.${}безопасен только для значений, которые выбрали вы сами, например для столбца из белого списка.[4] Spring Data Sort- Spring Data превращает
Sort.by("email")в ORDER BY по отображённым свойствам и отклоняет неизвестные имена, поэтому используйте его вместо ручного ORDER BY. Но ключи из запроса всё равно пропускайте через белый список, чтобы нельзя было сортировать по полям, которые вы не собирались открывать.[8]
Связи со стандартами
- Ссылка CWE для слабого места, описанного в этом руководстве: CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
Пошаговый сценарий атаки
Значение из запроса попадает в репозиторий
Обработчик Spring с @GetMapping принимает @RequestParam String id и передаёт его в findEmail(). Поскольку объявлен тип String, Spring не преобразует значение в другой тип; прежде чем полагаться на его формат, явно проверьте его.[9]
Исходный код: Уязвимый vs Защищённый вариант
// UserRepository.java: каждый запрос ниже склеивает данные запроса с текстом запроса
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 разбирает склеенную строку вместе со значением
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: склейка происходит до того, как Spring увидит SQL
public List<Long> findOrderIds(String status) {
return jdbc.queryForList("SELECT id FROM orders WHERE status = '" + status + "'", Long.class);
}
// JPA: сам JPQL собирается из ввода, а затем переводится в SQL
public List<User> findByEmail(String email) {
return em.createQuery("SELECT u FROM User u WHERE u.email = '" + email + "'", User.class)
.getResultList();
}
// поле ORDER BY берётся прямо из запроса
public List<User> listUsers(String sortField) {
return em.createQuery("SELECT u FROM User u ORDER BY u." + sortField, User.class)
.getResultList();
}
}
// UserRepository.java: значения — привязанные параметры, поля сортировки из белого списка
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: сначала компилируется маркер ?, значение задаётся типизированным сеттером
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: последние аргументы привязываются к маркерам ?
public List<Long> findOrderIds(String status) {
return jdbc.queryForList("SELECT id FROM orders WHERE status = ?", Long.class, status);
}
// JPA: именованный параметр, значение привязывается через 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();
}
// проверка по белому списку; в запрос попадают только константные фрагменты JPQL
public List<User> listUsers(String sortKey) {
String jpql = SORTED_QUERIES.getOrDefault(sortKey, SORTED_QUERIES.get("id"));
return em.createQuery(jpql, User.class)
.getResultList();
}
}
Чек-лист по защите системы для инженеров
- ✓Роняйте сборку на SpotBugs
SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTEи на правилах Find Security BugsSQL_INJECTION_JDBC,SQL_INJECTION_SPRING_JDBCиSQL_INJECTION_JPA.[10], [5] - ✓Отклоняйте любой
Statement.execute*,createQuery,createNativeQueryили вызов JdbcTemplate, где SQL-аргумент собран через+,String.formatилиStringBuilder.[2], [3], [7] - ✓Ищите
${в мапперах MyBatis в CI; каждое вхождение должно быть значением из жёстко заданного белого списка, задокументированного в маппере.[4] - ✓Стройте ORDER BY через
Sort.by()или белый список имён свойств вMap— никогда напрямую из@RequestParam.[8], [7] - ✓Объявляйте параметры запроса с фактическим доменным типом (
longдля числовых идентификаторов илиUUIDдля UUID), чтобы Spring мог отклонить ошибку преобразования до выполнения логики репозитория. Защиту от SQL-инъекции по-прежнему обеспечивает привязка параметров SQL.[9], [2]
Источники
- 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