説明
リクエスト由来の値をSQL、JPQL、HQLの文字列に連結したり埋め込んだりすると、データとクエリ構文の境界が失われます。攻撃者は条件式や追加の構文を挿入し、クエリの意味を変えられます。PreparedStatement を使っていても、入力を連結済みのSQLを prepareStatement に渡すだけでは安全になりません。クエリのテンプレートをサーバー側で定義し、外部の値はプレースホルダーへ別途バインドしてください。
想定される影響
- 許可されていないデータの取得や、認証・認可チェックの回避
- データの追加、変更、削除
- 高負荷のクエリによる可用性の低下
- 実際の影響範囲は、実行された文とデータベースアカウントの権限によって異なります。
対処方法
- SQL、JPQL、HQLの文字列はサーバー側で定義し、データ値は
PreparedStatement、JdbcTemplate、JdbcClient.param、JPA/HibernateのsetParameterでバインドしてください。 - プレースホルダーを使えない列名やソート方向は、
enum、switch、不変で有限の許可リストを使い、サーバーが定義した定数に対応付けてください。検査を通過しただけのリクエスト文字列を、そのまま連結しないでください。 - SQLのエスケープ、形式検証、名前だけで無害化を示唆するヘルパーを、パラメーターバインドの代わりにしないでください。
- データベースアカウントには、アプリケーションに必要な権限だけを付与してください。
例
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