19
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SQLインジェクションをPreparedStatementで防ぐ — 仕組みから理解する安全なクエリの書き方

19
Posted at

はじめに

以下のようなコードを見かけたことはないでしょうか。

String sql = "SELECT * FROM users WHERE name = '" + name + "'";

「動くし、テストも通っているから問題ない」と思いがちですが、これはSQLインジェクションの温床になります。

この記事では、PreparedStatement を使った対策に絞って、「なぜ安全なのか」を仕組みから解説します。


まず、何が危ないのか

こんなログイン処理を考えます。

// NG:ユーザー入力をそのまま連結している
String sql = "SELECT * FROM users WHERE name = '" + name + "' AND password = '" + password + "'";

name に以下の文字列を入力されたとします。

' OR '1'='1

SQLはこうなります。

SELECT * FROM users WHERE name = '' OR '1'='1' AND password = ''

'1'='1' は常にtrueなので、パスワードなしで全ユーザーの情報が取得できてしまいます。

なぜこうなるかというと、ユーザーが入力した文字列が「データ」ではなく「SQL命令の一部」として解釈されてしまうからです。この「データと命令の混在」こそがSQLインジェクションの本質です。


PreparedStatementとは

PreparedStatementは、SQL文の構造を先に確定させてから、値を後からバインドする仕組みです。

// OK:PreparedStatementを使う
String sql = "SELECT * FROM users WHERE name = ? AND password = ?";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setString(1, name);
ps.setString(2, password);
ResultSet rs = ps.executeQuery();

? の部分が「プレースホルダー」です。値は後からセットしますが、この時点でSQL文の構造はすでに確定しています。


なぜPreparedStatementは安全なのか

PreparedStatementが安全な理由は、値をバインドする際にJDBCドライバーが自動でエスケープ処理を行うからです。

先ほどの攻撃文字列 ' OR '1'='1 をバインドすると、内部では以下のように扱われます。

-- ドライバーがエスケープして、文字列リテラルとして安全に扱う
SELECT * FROM users WHERE name = '\' OR \'1\'=\'1' AND password = ''

シングルクォートがエスケープされるため、SQLの構造を壊すことができません。' OR '1'='1 はSQLとして解釈されず、ただの検索文字列として処理されます。


2段階で処理されることがポイント

PreparedStatementの内部処理は大きく2段階に分かれています。

① prepareStatement(sql)
   → DBサーバーにSQL文の構造を送信してコンパイルしてもらう
   → この時点でSQL文の「骨格」が確定する

② setString() / setInt() などで値をバインド
   → 値は「データ」としてのみ渡される
   → SQLの構造には一切影響しない

文字列連結との決定的な違いはここです。文字列連結は①と②が混在しているため、値がSQL命令として解釈される余地が生まれてしまいます。


Spring Boot(JPA)での書き方

実務でJPAを使っている場合も、根底にある考え方は同じです。

Spring Data JPAのメソッド名クエリ(最も安全)

public interface UserRepository extends JpaRepository<User, Long> {
    // 内部でPreparedStatementが使われる
    List<User> findByNameAndEmail(String name, String email);
}

メソッド名からクエリを自動生成する方式は、内部でプレースホルダーを使うため、SQLインジェクションを気にする必要がありません。

@Query アノテーションを使う場合

// OK:名前付きパラメーターを使う
@Query("SELECT u FROM User u WHERE u.name = :name")
List<User> findByName(@Param("name") String name);
// NG:文字列連結はしない
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)

nativeQuery = true を使う場面は生SQLを書くことになるため、プレースホルダーを必ず使うようにしましょう。


注意が必要なパターン

① 動的なWHERE句の追加

// NG:条件によってSQL文字列を組み立てている
String sql = "SELECT * FROM users WHERE 1=1";
if (name != null) {
    sql += " AND name = '" + name + "'";  // 危険
}
// OK:プレースホルダーで組み立てる
String sql = "SELECT * FROM users WHERE 1=1";
List<Object> params = new ArrayList<>();
if (name != null) {
    sql += " AND name = ?";
    params.add(name);
}
PreparedStatement ps = conn.prepareStatement(sql);
for (int i = 0; i < params.size(); i++) {
    ps.setObject(i + 1, params.get(i));
}

② ORDER BY句はプレースホルダーが使えない

ORDER BY のカラム名はプレースホルダーで渡せません(値ではなく識別子のため)。

// プレースホルダーは使えない(カラム名なのでエスケープ対象外)
String sql = "SELECT * FROM users ORDER BY ?";  // 意図した動きにならない

この場合は、許可するカラム名をホワイトリストで管理するのが安全策です。

private static final Set<String> ALLOWED_SORT_COLUMNS =
    Set.of("name", "email", "created_at");

public List<User> findAllSorted(String sortColumn) {
    if (!ALLOWED_SORT_COLUMNS.contains(sortColumn)) {
        throw new IllegalArgumentException("不正なソートカラム: " + sortColumn);
    }
    String sql = "SELECT * FROM users ORDER BY " + sortColumn;
    // ...
}

まとめ

PreparedStatementが安全な理由は一言でいうと、「SQL文の構造」と「バインドする値」を完全に分離しているからです。

文字列連結 PreparedStatement
SQL構造の確定タイミング 値を混ぜた後 値をバインドする前
値の扱い SQL命令の一部になりうる データとしてのみ扱われる
SQLインジェクション 発生しうる 防げる

Spring BootやJPAを使っていれば、多くの場面はフレームワークが守ってくれます。ただし nativeQuery や動的SQL、ORDER BY のカラム名指定など、「自分でSQL文字列を組み立てる場面」では必ず意識するようにしましょう。

ユーザーの入力値を文字列として直接SQLに連結していないか?」—— このチェックを習慣にしておくだけで、ほとんどのケースは防げます。

19
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?