설명
요청에서 유래한 값을 SQL, JPQL 또는 HQL 텍스트에 연결하거나 형식화하면 값과 쿼리 구문의 경계가 사라집니다. 공격자는 단순한 데이터 대신 조건식이나 추가 구문을 주입해 쿼리의 의미를 바꿀 수 있습니다. PreparedStatement를 사용하더라도 이미 오염된 완성 SQL을 prepareStatement에 넘기면 안전하지 않습니다. SQL 템플릿을 서버에서 정의하고 외부 값은 플레이스홀더에 별도로 바인딩해야 합니다.
잠재적 영향
- 애플리케이션이 허용하지 않은 데이터 조회 또는 인증·인가 검사 우회
- 데이터 추가, 변경 또는 삭제
- 비용이 큰 쿼리를 통한 서비스 가용성 저하
- 실제 영향 범위는 실행된 문장과 DB 계정에 부여된 권한에 따라 달라짐
해결 방법
- SQL, JPQL 및 HQL 텍스트는 서버에서 정의하고 모든 데이터 값은
PreparedStatement,JdbcTemplate,JdbcClient.param또는 JPA/HibernatesetParameter로 바인딩합니다. - 플레이스홀더로 바인딩할 수 없는 컬럼명이나 정렬 방향은
enum,switch또는 불변의 유한한 허용 목록을 사용해 서버가 정의한 상수로 매핑합니다. 검사를 통과한 요청 문자열 자체를 그대로 연결하지 않습니다. - SQL 이스케이프, 형식 검증 또는 이름만으로 정제 기능을 암시하는 도우미를 파라미터 바인딩의 대체 수단으로 사용하지 않습니다.
- DB 계정에는 애플리케이션에 필요한 최소 권한만 부여합니다.
예시
JDBC
변경 전
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import jakarta.servlet.http.HttpServletRequest;
public final class UserRepositoryUnsafe {
public boolean exists(HttpServletRequest request, Connection connection)
throws Exception {
String username = request.getParameter("username");
String requestedSort = request.getParameter("sort");
String sql = "SELECT id FROM users WHERE username = '" + username
+ "' ORDER BY " + requestedSort;
try (Statement statement = connection.createStatement();
ResultSet result = statement.executeQuery(sql)) {
return result.next();
}
}
}
username은 값 위치에, requestedSort는 식별자 위치에 삽입되므로 두 입력 모두 쿼리 구조를 바꿀 수 있습니다.
변경 후
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import jakarta.servlet.http.HttpServletRequest;
public final class UserRepositorySafe {
public boolean exists(HttpServletRequest request, Connection connection)
throws Exception {
String username = request.getParameter("username");
String requestedSort = request.getParameter("sort");
String sortColumn = switch (requestedSort) {
case "created" -> "created_at";
case "lastLogin" -> "last_login";
default -> "created_at";
};
String sql = "SELECT id FROM users WHERE username = ? ORDER BY "
+ sortColumn;
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, username);
try (ResultSet result = statement.executeQuery()) {
return result.next();
}
}
}
}
값은 ?에 바인딩하고, 바인딩할 수 없는 컬럼명은 요청 문자열을 재사용하지 않고 서버가 정의한 상수로 매핑합니다.
Spring JdbcClient
Spring Framework 6.1 이상에서 권장되는 JdbcClient도 sql(String)에 쿼리 텍스트를 받습니다. 안전한 API라는 이름만으로 동적 SQL 연결이 안전해지는 것은 아닙니다.
변경 전
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.jdbc.core.simple.JdbcClient;
public final class SpringUserRepositoryUnsafe {
private final JdbcClient client;
public SpringUserRepositoryUnsafe(JdbcClient client) {
this.client = client;
}
public void find(HttpServletRequest request) {
String name = request.getParameter("name");
client.sql("SELECT * FROM users WHERE name = '" + name + "'")
.query()
.listOfRows();
}
}
변경 후
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.jdbc.core.simple.JdbcClient;
public final class SpringUserRepositorySafe {
private final JdbcClient client;
public SpringUserRepositorySafe(JdbcClient client) {
this.client = client;
}
public void find(HttpServletRequest request) {
String name = request.getParameter("name");
client.sql("SELECT * FROM users WHERE name = :name")
.param("name", name)
.query()
.listOfRows();
}
}
JdbcTemplate은 ? 플레이스홀더와 메서드의 값 인자를, NamedParameterJdbcTemplate은 :name과 파라미터 맵을 같은 방식으로 사용합니다.
JPA/Hibernate
import jakarta.persistence.EntityManager;
import jakarta.servlet.http.HttpServletRequest;
public final class JpaUserRepositorySafe {
private final EntityManager entityManager;
public JpaUserRepositorySafe(EntityManager entityManager) {
this.entityManager = entityManager;
}
public void find(HttpServletRequest request) {
String name = request.getParameter("name");
entityManager.createQuery(
"select u.id, u.name from User u where u.name = :name",
Object[].class)
.setParameter("name", name)
.getResultList();
}
}
Hibernate Session.createSelectionQuery, createMutationQuery 및 createNativeMutationQuery도 외부 값을 HQL/SQL 텍스트에 연결하지 말고 setParameter로 바인딩해야 합니다. 이름이 지정된 쿼리도 정의된 쿼리 텍스트가 고정되어 있고 외부 값이 바인딩될 때만 이 문제를 방지합니다.
참조
- OWASP SQL Injection Prevention Cheat Sheet
- OWASP ASVS 5.0.0 V1.2.4
- OWASP Top 10:2025 A05 Injection
- Java SE 25
Statement - Java SE 25
Connection - Jakarta Persistence 3.2
EntityManager - Spring Framework 7.0.9
JdbcClient - Spring Framework 7.0.9
JdbcTemplate - Hibernate ORM 7.4 릴리스
- Hibernate ORM 7.4
QueryProducer - CWE-89
- CWE-564