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);
JavaSELECT * 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);
JavaPreparedStatement版:
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エンジニア”になっています。
