Java Tips | 文字列処理:SQLエスケープ

Java Java
スポンサーリンク

SQLエスケープは「文字列を“SQLの一部”として安全に扱う」ための技…なんだけど

最初に、いちばん大事なことから言います。

業務・実務では「SQLエスケープを書けるようになる」ことよりも、
「SQLエスケープを書かなくていい設計にする」ことのほうが圧倒的に重要です。

つまり、PreparedStatement(プレースホルダ付きSQL)を使う、ということです。

String name = request.getParameter("name");

String sql = "SELECT * FROM users WHERE name = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setString(1, name);
    ResultSet rs = ps.executeQuery();
}
Java

これが“正しい世界線”です。
この書き方をしている限り、「SQLエスケープ」という概念をほぼ意識しなくて済みます。

そのうえで、
「なぜエスケープが必要になるのか」「どういう危険があるのか」「どうして PreparedStatement が最強なのか」
を理解するために、あえて“生SQL+エスケープ”の話をしていきます。


なぜSQLエスケープが必要になるのか

文字列をそのままSQLに埋め込むと何が起きるか

例えば、こんなコードを考えてみます。

String name = request.getParameter("name");
String sql = "SELECT * FROM users WHERE name = '" + name + "'";
Java

ユーザーが Taro と入力したら、SQLはこうなります。

SELECT * FROM users WHERE name = 'Taro'

一見問題なさそうですよね。

でも、ユーザーがこんな文字列を入れてきたらどうなるでしょう。

' OR '1'='1

SQLはこうなります。

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

'1'='1' は常に真なので、全件取得されてしまいます。
これが典型的な SQLインジェクション です。

ここで必要になるのが「SQLエスケープ」――
「文字列の中に含まれる“SQLとして意味を持つ記号”を、ただの文字として扱わせる」 ための処理です。


SQLエスケープの基本:「シングルクォートを安全にする」

' を '' にする、というルール

SQLの文字列リテラルは ' で囲みます。

'abc'
'Tom & Jerry'

この中に ' 自体を含めたいときは、
SQLの世界では ''(シングルクォート2つ) と書きます。

'O''Reilly'  -- O'Reilly

つまり、文字列の中の ''' に置き換えれば、
「文字列の終わり」ではなく「文字としてのクォート」になる
、というルールです。

これをJava側でやるのが、いちばん基本的な「SQLエスケープ」です。


自前実装:シングルクォートだけをエスケープするユーティリティ

最低限の「文字列リテラル用エスケープ」

まずは、文字列を「SQLの '...' の中に安全に入れる」ためのユーティリティを書いてみます。

public final class SqlEscaper {

    private SqlEscaper() {}

    public static String escapeLiteral(String text) {
        if (text == null) {
            return null;
        }
        // ' を '' に置き換える
        return text.replace("'", "''");
    }
}
Java

使い方はこうなります。

String name = "O'Reilly";
String escaped = SqlEscaper.escapeLiteral(name);
String sql = "SELECT * FROM authors WHERE name = '" + escaped + "'";

System.out.println(sql);
// SELECT * FROM authors WHERE name = 'O''Reilly'
Java

ここで深掘りしたいポイントは二つです。

一つ目は、「SQLの文字列リテラルのルールに合わせて ''' にしているだけ」ということです。
O'Reilly をそのまま 'O'Reilly' と書くと、
'O'Reilly' に分断されてしまいますが、
'O''Reilly' と書けば、SQLは「O’Reilly」という1つの文字列として扱ってくれます。

二つ目は、「これは“最低限の安全策”であって、万能ではない」ということです。
データベースや文字コード、接続設定によっては、
他にもエスケープすべき文字やルールが存在します。
だからこそ、実務では PreparedStatement を使うほうが圧倒的に安全で楽 なのです。


例題:エスケープしないとどう壊れるかを体感する

' を含む名前でSQLが壊れるパターン

エスケープの必要性を実感するために、あえて「エスケープしない」例を見てみます。

String name = "O'Reilly";
String sql = "SELECT * FROM authors WHERE name = '" + name + "'";
System.out.println(sql);
Java

出力されるSQLはこうです。

SELECT * FROM authors WHERE name = 'O'Reilly'

SQLとして解釈すると、

  • 'O' という文字列
  • Reilly' という謎の残骸

になってしまい、構文エラーになります。

これを escapeLiteral を通すと、こうなります。

String name = "O'Reilly";
String escaped = SqlEscaper.escapeLiteral(name);
String sql = "SELECT * FROM authors WHERE name = '" + escaped + "'";
System.out.println(sql);
Java
SELECT * FROM authors WHERE name = 'O''Reilly'

SQLとして正しく解釈され、「O’Reilly」という1件だけが取得されます。

ここでのポイントは、「SQLエスケープは“構文を壊さないため”と“インジェクションを防ぐため”の両方に効いている」ということです。


でも実務では「SQLエスケープを書かない」ほうが正しい

PreparedStatement がなぜ“最強のSQLエスケープ”なのか

ここまで「エスケープの仕組み」を説明してきましたが、
実務であなたに身につけてほしいのは、「エスケープを自分で書かない設計」 です。

PreparedStatement を使うと、SQLと値が分離されます。

String name = request.getParameter("name");

String sql = "SELECT * FROM users WHERE name = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setString(1, name); // ← ここでDBドライバが適切にエスケープしてくれる
    ResultSet rs = ps.executeQuery();
}
Java

ここで重要なのは、

  • sql 文字列の中には、値が一切埋め込まれていない
  • ? の部分に値を渡すとき、DBドライバが“文字列リテラルとして安全な形”に変換してくれる

という2点です。

つまり、「SQLエスケープの正しいやり方を、あなたが覚える必要がなくなる」 のです。
これが、PreparedStatement を使う最大のメリットです。


例題:PreparedStatement と「手書きエスケープ」の違いを比べる

同じことをしているようで、安心感がまったく違う

手書きエスケープ版:

String name = request.getParameter("name");
String escaped = SqlEscaper.escapeLiteral(name);
String sql = "SELECT * FROM users WHERE name = '" + escaped + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
Java

PreparedStatement版:

String name = request.getParameter("name");
String sql = "SELECT * FROM users WHERE name = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, name);
ResultSet rs = ps.executeQuery();
Java

どちらも「ユーザー名で検索する」という意味では同じですが、

  • 手書き版は、エスケープ漏れ・バグ・DBごとの仕様差 に常に怯えることになります。
  • PreparedStatement版は、「値は値として扱われる」ことが保証される ので、SQLインジェクションの心配がほぼなくなります。

ここで身につけてほしい感覚は、
「SQLエスケープを理解したうえで、“だからこそ PreparedStatement を使う”と腹落ちさせること」 です。


どうしても「生SQLを組み立てる」必要があるときの心構え

LIKE句や動的なIN句など、グレーゾーンの世界

現場では、どうしても「SQL文字列を組み立てざるを得ない」場面もあります。

例えば、LIKE句のワイルドカードを含めたいとき。

String keyword = request.getParameter("keyword");
// ユーザー入力に % や _ が含まれると、LIKE の意味が変わる
String pattern = "%" + keyword + "%";
String sql = "SELECT * FROM items WHERE name LIKE ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, pattern);
Java

この場合も、PreparedStatement を使いつつ、
「LIKE用のエスケープ(% や _ をリテラルとして扱う)」を別途考える必要があります。

あるいは、IN句を動的に組み立てるとき。

List<String> ids = List.of("A", "B", "C");
// "?,?,?" のようなプレースホルダを動的に作る
String placeholders = String.join(",", Collections.nCopies(ids.size(), "?"));
String sql = "SELECT * FROM items WHERE id IN (" + placeholders + ")";
PreparedStatement ps = conn.prepareStatement(sql);
for (int i = 0; i < ids.size(); i++) {
    ps.setString(i + 1, ids.get(i));
}
Java

ここでも、SQL文字列自体は組み立てているけれど、値は必ずプレースホルダ経由で渡している、というのがポイントです。

「どうしても生SQLを組む」場面でも、
“値を直接連結しない”という原則だけは絶対に守る――
これが、SQLエスケープの本質的な安全策です。


まとめ:SQLエスケープユーティリティで本当に身につけてほしいこと

SQLエスケープそのものは、技術的にはシンプルです。
文字列リテラルの中で ''' にする、それだけです。

でも、業務・実務で本当に大事なのは、

  • 「なぜエスケープが必要になるのか」
  • 「エスケープを忘れると何が起きるのか」
  • 「だからこそ PreparedStatement を使うべきなのか」

を、自分の言葉で説明できるようになること です。

もしあなたのコードのどこかに、

String sql = "SELECT ... WHERE name = '" + userInput + "'";
Java

のような行があったら、
それを題材にして、

  • まずは SqlEscaper.escapeLiteral を挟んでみる
  • そのうえで、「これ、PreparedStatement に書き換えられない?」と自分に問いかけてみる

というステップを踏んでみてください。

SQLエスケープを“書けるようになる”ことはゴールではなく、
「エスケープを書かなくていい設計にたどり着くための通過点」 です。
そこまでたどり着けたら、もう一段上の“業務で戦えるJavaエンジニア”になっています。

タイトルとURLをコピーしました