はじめに
こんにちは。
プログラミング初心者wakinozaと申します。
勉強中に調べたことを記事にまとめています。
十分気をつけて執筆していますが、なにぶん初心者が書いた記事なので、理解が浅い点などあるかと思います。
間違い等あれば、指摘いただけると助かります。
記事を参考にされる方は、初心者の記事であることを念頭において、お読みいただけると幸いです。
動作環境
- Windows11
- Oracle Java 21
- eclipse 2025 (pleiades)
- MySQL 8.4.6
記事のテーマ
- Webアプリケーションのセキュリティについて勉強しています。
- この記事では、Java(Servlet/JSP)でMySQLを利用する際のSQLインジェクション対策についてまとめます。
目次
1. SQLインジェクションとは
2. 対策方法
3. MySQL利用時の注意点
1. SQLインジェクションとは
「SQLインジェクション」は、アプリケーションの脆弱性を利用して、悪意のあるSQLコマンドをデータベースに「注入(インジェクション)」する攻撃です。
データベースと連携したウェブアプリケーションの多くは、ユーザーからの入力情報を基にSQL文(データベースへの命令文)を組み立てます。
もしセキュリティ対策が不十分である場合、インジェクション攻撃によってデータベースの不正利用をまねく可能性があります。このような問題を「SQLインジェクションの脆弱性」と呼び、問題を悪用した攻撃を、「SQLインジェクション攻撃」と呼びます。
SQLインジェクションの原因は、「リテラル」の扱いにあります。
「リテラル」とは、ソースコード中に直接記述された、文字列や数値などのデータのことを指します。
1-1. 文字列リテラルでの問題
SQL文では、いくつかの文字(' や -- など)に特殊な意味を持たせています。攻撃者はこの特殊文字を利用して攻撃を仕掛けます。
「'(シングルクオート)」を例に出して説明します。
SQL文中の文字列リテラルは、「'(シングルクオート)」で囲むことで、「文字列」と認識されます。
SELECT * FROM users WHERE username = 'foo'
文字列リテラルの中にさらにシングルクオートを含めたい場合は、シングルクオートを2つ続けて「''」のように記述します。2つ続けて記述することで、「これは文字列の囲みではなく、1つの文字である」とデータベースに認識させることができます。これを「シングルクオートをエスケープする」と言います。
SELECT * FROM users WHERE username = 'foo''bar'
しかし、リテラル中のシングルクオートが文字としてのシングルクオートなのか、文字列リテラルの終端としてのシングルクオートなのかを、プログラムが適切に判別することは困難です。
そのため、以下のようなことが起きます。
SELECT * FROM users WHERE username = 'foo'bar'
「o」と「b」の間のシングルクオートによって、文字列リテラルが終了したと認識されています。そのため、「foo」は文字列と認識されていますが、後に続く「bar」部分は文字列と認識されず、SQLの平文として認識されています。
「bar」はSQL文として意味は持たないので上のSQL文は構文エラーを起こすだけで済みます。しかし、ここにSQL文として意味がある文字列を挿入すると、SQL文に任意の命令を追加することができます。
これが、SQLインジェクションです。
例として、username(ユーザー名)とpassword(パスワード)を受け取り、MySQLのusersテーブルと照合するSQL文を見てみましょう。
SELECT * FROM users WHERE username = 'foo' AND userpass = 'foopassword';
もしこの時、ユーザー入力欄に以下の文字列が入力されたらどうなるでしょう。
- username : ' OR '1'='1' --
- userpass : foo
上のユーザー入力情報を受け取ったSQLは以下の通りになります。
SELECT * FROM users WHERE username = '' OR '1'='1' -- ' AND userpass = 'foo';
入力情報の一部がSQLの平文として認識されています。
「foo」の部分は特に意味はありません。
重要なのは、「OR '1'='1'」の部分がSQL文として成立している点です。
「'1'='1'」は常にtrueとなるため、「OR」を組み合わせると、常にtrueの条件式が出来上がります。
また、「--」以降の部分も文字列と認識されず、SQLの平文と認識されています。「--」という文字列は、SQL文では「コメントアウト」を意味するため、それ以降のパスワードの照合部分がコメントアウトされてしまいました。
その結果、WHERE句が常にtrueの「SELECT * FROM users WHERE true」と同様のSQL文が組み立てられ、usersテーブルのすべての情報が認証なしに検索されてしまいます。
1-2. 数値リテラルでの問題
次に数値リテラルでの問題を説明します。
Webアプリケーションにおいて、ユーザーからの入力値は通常文字列として受け取られます。これを数値として扱う際に、適切なキャストやバリデーションを行わず直接SQL文に結合すると、攻撃者に任意のSQL文を記述させる隙を与えてしまいます。
以下は、生徒一覧テーブル(students)から、指定の学年以上の情報を検索するSQL文です。
SELECT * FROM students WHERE grade >= $grade
もしこの時、ユーザー入力欄に以下の文字列が入力されたらどうなるでしょう。
- grade : 1; dELETE FROM students
SELECT * FROM students WHERE grade >= 1; DELETE FROM students
SQL文において、数値リテラルはシングルクオートで囲まないため、数値以外の文字が現れた時点で数値リテラルは終了し、以降はSQLの平文として認識されてしまいます。
このように、どこまでがリテラルでどこからがSQL文かを誤認させることで、攻撃者は任意のSQL文を挿入する。
これがSQLインジェクションです。
2. 対策方法
では、SQLインジェクションを防ぐためにはどうすればよいのでしょう?
この章では具体的な3つの対策とコード例、そしてその他にリスクを最小限にするために実施すべき対策を3つ紹介します。
2-1. プレースホルダでSQLを組み立てる
SQL文を組み立てる際に、「プレースホルダ」を利用することで、SQLインジェクションを防ぐことができます。
SQLを組み立てる方法は大きく分けて2つあります。
1つ目は、SQLの命令とユーザーからの入力データなどを文字列として結合する方法。
もう一つが、SQLの命令にプレースホルダを設定する方法です。
1つ目の文字列結合は、SQLインジェクションの脆弱性があるため、非常に危険な記述方法です。
例えば以下のようなコードは、SQLインジェクションのリスクが非常に高いため、実務での使用は厳禁です。
//非推奨コード
// ユーザーからの入力を受け取る
String user = request.getParameter("username");
String pass = request.getParameter("password");
// SQLを文字列として結合
String sql = "SELECT * FROM users WHERE username = '" + user + "' AND password = '" + pass + "'";
Statement stmt = conn.createStatement();
//文字列結合したSQL文を利用してDBに検索を行う
ResultSet rs = stmt.executeQuery(sql);
もう一つの方法では、入力値を「?」としてSQL文を記述します。
この「?」の部分のことを、後から入力情報を格納する場所(place)を確保(hold)しているという意味で「プレースホルダ」と呼び、「プレースホルダ」に入力値を格納することを「バインドする」といいます。
Java言語でプレースホルダを利用する場合は、PreparedStatementインターフェースを利用します。
String user = request.getParameter("username");
String pass = request.getParameter("password");
// 1. SQLのテンプレートを作成(? がプレースホルダ)
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
// 2. PreparedStatementを作成(この時点でSQLの形が固定される)
PreparedStatement pstmt = conn.prepareStatement(sql);
// 3. ?プレースホルダに安全に値をセット
// 万が一値に攻撃コードが入っていても、ただの文字列として処理される
pstmt.setString(1, user);
pstmt.setString(2, pass);
// 4. 実行
ResultSet rs = pstmt.executeQuery();
また、プレースホルダにも、いつバインドするかによって2種類の方式があります。
「動的プレースホルダ」と、「静的プレースホルダ」です。
動的プレースホルダは、アプリケーション側でバインドしてから、データベースにSQL文を送信する方法です。
アプリケーションのライブラリが入力情報を適切にエスケープし、SQLにバインドします。命令文と入力情報を結合してからデータベースに送っているという点では、非推奨の文字列結合と同じです。しかし、結合前に、ライブラリによる厳密なエスケープ処理を行っているため、SQLインジェクションのリスクを大幅に低減します。
もう一つの静的プレースホルダは、データベース側でバインドを行います。
まず実行したいSQLの雛形(スケルトン)を予めデータベースに送信し、実行計画を確定させます。その後、入力値を別送してバインドするため、入力値がSQLの構造を変化させることは不可能です
また、入力値はすべて「文字列」として扱われます。特殊文字を使っていても、SQLインジェクションを目的とした入力情報だとしても、必ず文字列として扱われます。入力情報をSQL文と認識することはありませんし、すでにプリコンパイルされた命令文を変更することもありません。つまり、入力値によってSQL文が書き換え得られる事態を完全に防ぐことが可能なのです。
動的も静的もどちらも非常に安全なシステムですが、完全にSQLインジェクションを防ぐことができるという点で、「静的プレースホルダ」がより推奨されます。
動的プレースホルダも現状十分に安全ではありますが、その安全性はライブラリの精度に依存しています。ライブラリの実装にバグがあったり、全く新しい入力パターンの攻撃が行われた際にSQLインジェクションを完全に防げない可能性がわずかに存在します。
そのため、パフォーマンス等の理由がなければ、静的プレースホルダを実装することが推奨されます。
2-2. 入力値をチェック
基本的にプレースホルダを利用することで、SQLインジェクションを防ぐことができますが、「ORDER BY」などプレースホルダが利用できない状況では、開発者が入力値のチェックを行う必要があります。
とはいえ、多くのアプリケーションには、不正な値を防ぐためバリデーションがすでに実装されていると思います。SQLインジェクション対策のチェックは、バリデーションをより厳密にしたものと考えると理解しやすいと思います。
必要な対策は、
1、データ型や文字数のチェック
2、ホワイトリスト方式の制限
などがあります。
1つ目は「データ型や文字数のチェック」です。
バリデーションと重複しますが、数値のデータ型に文字列が格納されていないか、指定した最大文字数をオーバーしていないかなどをチェックします。
SQLインジェクションは入力文字数が長くなる傾向があります。仮に最大文字数を10文字に設置してバリデーションを行うと、SQLインジェクションのような長い記述を受け付けなくなります。
データ型を検証し、不必要に長い文字列を拒絶するよう制限を加えるだけでも、攻撃の試行を困難にし、リスクを低減できます。
次は、「ホワイトリスト方式の制限」です。
1のチェックをクリアしても、不正な入力情報が紛れ込む可能性は高いです。そのため、不正な情報が入力されていないかを確認します。
とはいえ、ライブラリのような厳密なエスケープ処理を自作するのは困難なので、許可できる入力情報をまとめた「ホワイトリスト方式」で入力情報をチェックします。
String sort = request.getParameter("sort");
String sqlOrder = "";
if ("id".equals(sort)) {
sqlOrder = "id";
} else if ("date".equals(sort)) {
sqlOrder = "due_date";
} else {
sqlOrder = "id"; // デフォルト
}
String sql = "SELECT * FROM todos ORDER BY " + sqlOrder;
上のコードでは入力情報をチェックし、指定された文字列でない場合は、デフォルトの文字列に変更してから、SQL文に結合しています。
これにより、不正な入力情報をSQLに格納する可能性がなくなります。
2-3. 文字エンコーディングの設定
SQLインジェクション対策において、文字エンコーディングの設定は非常に重要です。
特に、「動的プレースホルダ」が利用される環境では、文字コードの特性を悪用したエスケープ回避攻撃が可能になるためです。
「Shift_JIS」などのマルチバイト文字体系では、エスケープ処理でシングルクオートを無効にする「\(バックスラッシュ)」のコードは「0x5c」と表現されます。そして、一部の2バイト文字は、コードの後半に「0x5c」を利用しています。(「0x83」と「0x5c」)で一文字を形成する、など)
この性質を利用したのが、「5c問題」です。
1、攻撃者は「'」の前に、「0x5c」と組み合わせると1文字を形成するコードの前半部分を記述する。
2、エスケープ処理が、「'」の前に「\(0x5c)」を挿入する
3、データベース側がこのバイト列を読み込む際、あらかじめ適切なエンコードを設定していないと、あらかじめ「'」の前に置いていたコードと、エスケープ処理で挿入された『\(0x5c)』が組み合わさって1つの2バイト文字として解され、結果としてエスケープの役割が消失してしまいます。
4、結果として、無効化されるはずだった「'」が有効な状態(エスケープが外れた状態)でデータベースに渡ってしまい、インジェクションが成立する。
これが「5C問題」です。
「5C問題」はPHPで有名になった問題ですが、Javaにおいてもドライバの設定次第で潜在的なリスクとなり得ます。
このような文字エンコーディングを利用した攻撃を防ぐには、文字コードに「UTF-8」を指定することが有効です。
UTF-8は、マルチバイト文字を構成するバイト列の中に、ASCII範囲のコード(0x00〜0x7F)が一切出現しないよう設計されています。そのため、エスケープ文字(0x5c)とマルチバイト文字の一部が混同されることはありません。
コードでは、MySQLへ接続URLに「characterEncoding=UTF-8」というパラメータを追加することで、文字コードに「UTF-8」を指定することができます。
jdbc:mysql://localhost:3306/test_db?characterEncoding=UTF-8
なお、この文字エンコーディングの不一致を突く攻撃は、静的プレースホルダを用いれば、構造的に完全に防ぐことが可能です。
文字エンコーディングを使用したSQLインジェクションを防ぐためにも、動的プレースホルダより静的プレースホルダを選択するほうが望ましいでしょう。
2-4. 権限を最小化する
以下は、直接SQLインジェクションを防止するわけではありませんが、同時に実施することが望ましい対策を3つ紹介します。
1つ目が、データベースアカウントの権限を最小化することです。
データベース接続に使用するアカウントの権限が必要以上に広い場合、攻撃受けた際に被害が深刻化する恐れがあります。
最上位権限を持つrootユーザーは利用せず、必要最小限の権限を付与したアカウントを設定することで、もし攻撃を受けた際の被害を最小限に抑えることができます。
2-5. ウェブアプリケーションに渡されるパラメータにSQL文を直接指定しない
情報処理推進機構のHPで公開されている『安全なウェブサイトの作り方』という資料には、hiddenパラメータ等にSQL文をそのまま指定してSQLインジェクションを受けた例が報告されています。
パラメータにSQL文を指定するということは、プログラムの内部構造を外部(クライアントのブラウザ)に流出するのと同義です。SQLがブラウザに流出してしまうと、攻撃者はSQL文を自由に書き換えることができます。
例えば、書き換えたURLでGETリクエスト送ったり、開発者ツールでパラメータ値を書き換えることもできます。
SQL文はサーバに保管し、ユーザーからは入力情報だけを受け取るという体制が必ず必要です。
2-6. エラーメッセージをそのままブラウザに表示しない
エラーメッセージには、データベースの種類、エラーの原因、エラーを起こしたSQL文の情報が含まれる場合があります。
そのため、エラーメッセージをブラウザ側に表示してしまうと、攻撃者にSQLインジェクションにつながる情報を与えてしまいます。
エラーメッセージは、ユーザーのブラウザ上に表示させないことが原則です。
3. MySQL利用時の注意点
先ほど紹介したJava言語のPreparedStatementインターフェースは静的プレースホルダを実装しています。
しかし、データベースにMySQLを利用する場合は、注意が必要です。
Java言語のPreparedStatementインターフェースは静的プレースホルダを実装していますが、MySQLのJDBCドライバは、「動的プレースホルダ」がデフォルトとなっています。そのため、せっかくPreparedStatementインターフェースを記述していても、内部では動的プレースホルダで処理されてしまいます。
MySQLで動的プレースホルダがデフォルトになっている理由は、「静的プレースホルダ」には、データベース側にSQL文を保持するためのメモリが必要になったり、アプリケーションとデータベース間での送信回数が増加しパフォーマンスが低下するという欠点があるからです。
MySQLで静的プレースホルダを利用したい場合は、そういった欠点を考慮に入れた選択する必要があります。
Java言語とMySQL環境で静的プレースホルダを実現するには、データベース接続URLに「useServerPrepStmts=true」というパラメータを追加します。
jdbc:mysql://localhost:3306/test_db?useServerPrepStmts=true
まとめ
-
脆弱性の本質: SQLインジェクションは、リテラルの解釈ミスを利用して「データ」を「命令」へと変える攻撃であり、ブラックリスト方式のエスケープ処理では完全に防ぐことは困難である。
-
静的プレースホルダの優位性: データベース側で実行計画を確定させる静的プレースホルダは、物理的に命令の書き換えを許さないため、最も堅牢な対策となる。
-
文字エンコーディングの影響: Shift_JIS等のマルチバイト文字体系では、バイトレベルの解釈の不一致(5c問題)によりエスケープを回避されるリスクがある。UTF-8の一貫した利用と、接続URLでの明示が必須である。
-
MySQL特有の挙動: MySQL JDBCドライバはデフォルトで動的プレースホルダを採用しているため、セキュリティとパフォーマンスのトレードオフを理解した上で useServerPrepStmts=true 等の適切な設定を行う必要がある。
記事は以上です。
最後までお読みいただき、ありがとうございました。
参考情報一覧
この記事は以下の情報を参考にして執筆しました。
- [体系的に学ぶ 安全なWebアプリケーションの作り方 第2版]
- [スッキリわかるJava入門 実践編 第4版]
- [スッキリわかるサーブレット&JSP入門 第4版]
- [スッキリわかるSQL入門 第3版]
- [MySQL徹底入門 第5版]
- [基礎からのサーブレット/JSP 第5版]
- 【セキュリティ】SQLインジェクション対策の基本:プレースホルダーを正しく使う (最終更新 2026-01-01) (参照 2026-01-22)
- SQLインジェクションとは (最終更新 2019-09-10) (参照 2026-01-22)
- 文字コードの5C問題について解説 (最終更新 2022-09-05) (参照 2026-01-22)
- 安全なウェブサイトの作り方 - 1.1 SQLインジェクション (参照 2026-1-22)
- 安全なSQLの呼び出し方 (参照 2026-1-22)