文字列連結によるSQLインジェクション

動的に組み立てたSQL、JPQL、HQLへのリクエスト値の挿入

説明

リクエスト由来の値をSQL、JPQL、HQLの文字列に連結したり埋め込んだりすると、データとクエリ構文の境界が失われます。攻撃者は条件式や追加の構文を挿入し、クエリの意味を変えられます。PreparedStatement を使っていても、入力を連結済みのSQLを prepareStatement に渡すだけでは安全になりません。クエリのテンプレートをサーバー側で定義し、外部の値はプレースホルダーへ別途バインドしてください。

想定される影響

  • 許可されていないデータの取得や、認証・認可チェックの回避
  • データの追加、変更、削除
  • 高負荷のクエリによる可用性の低下
  • 実際の影響範囲は、実行された文とデータベースアカウントの権限によって異なります。

対処方法

  • SQL、JPQL、HQLの文字列はサーバー側で定義し、データ値は PreparedStatement、JdbcTemplate、JdbcClient.param、JPA/Hibernateの setParameter でバインドしてください。
  • プレースホルダーを使えない列名やソート方向は、enum、switch、不変で有限の許可リストを使い、サーバーが定義した定数に対応付けてください。検査を通過しただけのリクエスト文字列を、そのまま連結しないでください。
  • SQLのエスケープ、形式検証、名前だけで無害化を示唆するヘルパーを、パラメーターバインドの代わりにしないでください。
  • データベースアカウントには、アプリケーションに必要な権限だけを付与してください。

例

JDBC

変更前

java
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 は識別子の位置に挿入されるため、どちらの入力もクエリ構造を変える可能性があります。

変更後

java
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連結が安全になるわけではありません。

変更前

java
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();
    }
}

変更後

java
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

java
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 でバインドしてください。名前付きクエリも、定義されたクエリ文字列が固定され、外部の値がバインドされている場合に限り、この問題を防ぎます。

参考資料