1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[MySQL]SQLインジェクションについて[Servlet/JSP]

Last updated at Posted at 2026-01-23

はじめに

こんにちは。
プログラミング初心者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」を指定することができます。

context.xml
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」というパラメータを追加します。

context.xml
jdbc:mysql://localhost:3306/test_db?useServerPrepStmts=true

まとめ

  • 脆弱性の本質: SQLインジェクションは、リテラルの解釈ミスを利用して「データ」を「命令」へと変える攻撃であり、ブラックリスト方式のエスケープ処理では完全に防ぐことは困難である。

  • 静的プレースホルダの優位性: データベース側で実行計画を確定させる静的プレースホルダは、物理的に命令の書き換えを許さないため、最も堅牢な対策となる。

  • 文字エンコーディングの影響: Shift_JIS等のマルチバイト文字体系では、バイトレベルの解釈の不一致(5c問題)によりエスケープを回避されるリスクがある。UTF-8の一貫した利用と、接続URLでの明示が必須である。

  • MySQL特有の挙動: MySQL JDBCドライバはデフォルトで動的プレースホルダを採用しているため、セキュリティとパフォーマンスのトレードオフを理解した上で useServerPrepStmts=true 等の適切な設定を行う必要がある。


記事は以上です。
最後までお読みいただき、ありがとうございました。

参考情報一覧

この記事は以下の情報を参考にして執筆しました。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?