COLUMN

SQLインジェクションとは?発生原理と対策を解説

ITシステムの情報セキュリティ対策でお悩みや課題をお持ちの方いましたらNTT東日本のセキュリティエンジニアにてご相談におこたえしておりますのでぜひお気軽にお問い合わせください。

NTT東日本のセキュリティ診断チームです。

多くのWebアプリケーションではデータベースへのアクセスのためにSQLを利用しています。SQLはデータベース内のデータを取得したり、検索したりするために使用します。SQL文の組み立てに不備があると、SQLインジェクションという脆弱性が発生します。SQLインジェクションはNTT東日本のセキュリティ診断でも検出され、リスクが高い脆弱性になります。 今回のコラムではSQLインジェクションの発生原理と対策を解説しますので、ぜひ最後までお読みください。

1. SQLインジェクション攻撃とは

攻撃者が悪意のあるリクエストをWebアプリケーションに送信することにより、データベース(以下「DB」とする。)を不正に利用する攻撃です。DBの不正利用としては、不正なデータの閲覧、追加、更新、削除などがあります。攻撃による具体的な被害例は、DBの設定や使用方法などによりさまざまです。たとえば、SQLインジェクションに対して脆弱なSQL文を用いて認証部分を構築している場合を考えます。このような場合には、攻撃により認証を回避したログインや、IDやPWのようなクレデンシャル情報に対する改ざんなどが想定できます。2023 CWE Top 25においてもSQLインジェクション(CWE-89)は第3位に選ばれており、たいへん危険な攻撃です。

1-1. 発生原理

SQLインジェクションは、SQL文を組み立てる際に、メタ文字を適切に処理しないために発生します。以下に、発生原理の図を示します。

上記の例では、SQL文を組み立てる際に、クライアントから入力される変数idの値をそのまま用いています。変数idに含まれる単一引用符(')はSQL文における文字列リテラルを終端させます。そのため、変数idの残りの部分であるor TRUE; --がWHERE句の条件式の一部となり、SQL文が強制的に変更されています。最終的に生成されるSQL文は以下のようになります。

SELECT * FROM users WHERE id = '' or TRUE; --;

SQLでは、--以降はコメントアウトされます。本来は、単一のidと合致するデータを取得するSQL文でしたが、条件id = '' or TRUEは常に真となるためusersテーブルに格納されるすべてのデータを取得するSQL文となってしまいました。今回の例では、条件を変更するのみでしたが、データの追加や削除を行うSQL文とすることも可能です。

ITシステムの情報セキュリティ対策でお悩みや課題をお持ちの方いましたらNTT東日本のセキュリティエンジニアにてご相談におこたえしておりますのでぜひお気軽にお問い合わせください。

2. SQLインジェクションの実演

実際に、簡単なシステムを構築して、SQLインジェクションを再現してみます。構成は以下の通りです。今回は、一般的なWebアプリケーションにあるようなメールアドレスとパスワードを検証して認証を行う部分にSQLインジェクション脆弱性があるとします。

入力画面について説明します。 ブラウザからメールアドレスとパスワードを入力する画面は以下のようになります。「ログイン」を押下すると、メールアドレスとパスワードがWebサーバーに送信されます。

Apache HTTP Server上で実行するPHPの実装について説明します。今回はクライアントからの入力を受け取るとPHPで処理してSQL文を組み立てます。実装は以下の通りです。ただし、一部簡略化して掲載しているため、そのままでは動作しません。また、以下のコードはSQLインジェクションを実演するためだけの実装であるため、認証部分の実装としてはセキュリティ的な観点からは危険な実装です。絶対に認証の実装としては使用しないでください。

PHPでは終止符(.)は文字列結合を行う結合演算子です。クライアントから送信されたメールアドレスとパスワードが一致するデータがusersテーブルに1つ以上あれば認証成功となる実装となっています。

DBサーバーのusersテーブルに格納されているデータは以下の通りです。

+----+----------------+--------+
| id | mail           | pass   |
+----+----------------+--------+
|  1 | siat-1@exmaple | pass01 |
|  2 | siat-2@exmaple | pass02 |
|  3 | siat-3@exmaple | pass03 |
|  4 | siat-4@exmaple | pass04 |
|  5 | siat-5@exmaple | pass05 |
+----+----------------+--------+

こちらも、SQLインジェクションの実演のみを目的にしたテーブルであるため、パスワードを平文で保存しているなど、セキュリティ的な観点からは危険な実装です。ここでは、単純にクレデンシャル情報が保存されているDBと認識してください。

2-1. 正常なログイン試行

ログインを試してみます。ブラウザからメールアドレスとパスワードを入力してみます。まずは、認証に成功する例です。下の画像の通りに入力し、「ログイン」ボタンを押下します。

このとき、認証に成功しブラウザ画面は以下のような表示になります。

続いて、誤ったパスワードを入力して認証失敗した場合を見てみます。以下のように誤ったパスワードを入力します。

「ログイン」ボタンを押下すると、「パスワードが違います。」と表示され、以下のように認証に失敗します。

上述の認証成功した時と失敗した時で、実際にPHPで組み立てられたSQL文を観察します。DBサーバーに保存されているログを確認してみると以下のようになります。

Time                 Id Command    Argument
2024-03-08T08:43:54.748317Z         9 Execute   SELECT count(*) FROM users WHERE mail='siat-1@example' and pass='pass01'
2024-03-08T08:44:00.745226Z        10 Execute   SELECT count(*) FROM users WHERE mail='siat-1@example' and pass='wrongpass'

PHPにおいて、クライアントからのメールアドレス(siat-1@example)とパスワード(pass01またはwrongpass)の値をそれぞれ取得し、SQL文を組み立てられていることが確認できます。

2-2. SQLインジェクションを悪用した認証回避

SQLインジェクションの実演として、認証回避を行います。メールアドレスとしてsiat-1@example、パスワードとして'or TRUE;--を入力してみます。ブラウザ画面は以下のようになります。

siat-1@exampleに対応するパスワードはpass01であるため、本来であればこの場合は認証に失敗するはずです。しかし、実際に「ログイン」ボタンを押下してみると以下の通り画面遷移が発生しており、認証に成功してしまったことがわかります。

PHPにおけるSQL文の組み立てにSQLインジェクション脆弱性が存在したために、認証を回避されてしまいました。では、どのように認証回避が行われたのか紐解いていきます。まず、DBサーバー上でどのようなSQL文が実行されたのか確認します。DBサーバーのログファイルを見ると以下のSQL文が実行されたことがわかります。

Time                 Id Command    Argument
2024-03-11T05:43:03.415036Z        20 Query     SELECT count(*) FROM users WHERE mail='siat-1@example' and pass='' or TRUE;

上記のSQL文のWHERE句は、(A and B) or TRUEという構造になっており、常に真となるため、パスワードが誤っているにもかかわらず、テーブルに格納されている全データの個数である5が出力されます。今回のPHPの実装では、SQL文を実行して出力された値が0より大きければ、認証に成功という判定です。そのため、正しいパスワードを入力していないにも関わらす、認証に成功しています。では、なぜそのようなSQL文になってしまったのでしょうか?PHPにおけるSQL文を組み立てるソースコードは以下の通りです。

$sql = "SELECT count(*) FROM users WHERE mail='" . $mail . "' and pass='" . $pass . "';";

上記のソースコードに変数mailとpassの値をそれぞれ代入し、文字列結合をしてみると以下のようになります。

$sql = "SELECT count(*) FROM users WHERE mail='siat-1@example' and pass='' or TRUE;--';";

結局、組み立てられたSQL文は以下の通りです。

SELECT count(*) FROM users WHERE mail='siat-1@example' and pass = '' or TRUE;--';

前述したように、--以降はコメントアウトされます。結果的に、DBサーバーのログで確認したSQL文となります。

ITシステムの情報セキュリティ対策でお悩みや課題をお持ちの方いましたらNTT東日本のセキュリティエンジニアにてご相談におこたえしておりますのでぜひお気軽にお問い合わせください。

3. SQLインジェクションに対する対策

IPAの資料に基づく根本的対策として、以下の3つがあげられます。

  • バインド機構(プレースホルダ)を用いたSQL文の組み立て
  • クライアントからの入力に対してエスケープ処理を適切に行う
  • 実行するSQL文をクライアントから直接受け付けない

1つ目の対策について説明します。まず、SQL文に値を埋め込む部分をプレースホルダとしてあらかじめ指定しておきます。SQL文を組み立てる際は、クライアントからの値をそのプレースホルダに対して機械的に値を割り当てる(バインドする)という方法です。

2つ目の対策は、SQL文におけるメタ文字をエスケープ処理する方法です。例えば、MySQLの場合では、単一引用符(')は\'に変換する処理を行います。こうすることで、文字列リテラルの開始あるいは終端としてのメタ文字ではなく、単一引用符という文字として扱うことができます。すべてのメタ文字を適切にエスケープ処理する必要があるため、自身で実装する際には注意が必要です。

3つ目の対策としては、そもそもSQL文自体をクライアントに指定させない実装とするというものです。

今回は、バインド機構(プレースホルダ)を利用してSQL文を組み立てることで、SQLインジェクションに対する対策を実装します。PHPの実装は以下の通りです。以下の図の赤枠で囲った部分が、元のソースコードからの変更点です。3行目で、SQL文のmailとpassに割り当てるためのプレースホルダとしてそれぞれ:mail, :passと設定しています。4行目は変数をバインドするための処理です。5、6行目で、実際にクライアントから受け取った値をそれぞれSQL文にバインドしています。使用している関数の詳細については、こちらのドキュメントを参照してください。

修正後、再び、以下のようにブラウザに入力します。

「ログイン」ボタンを押下すると、以下のように認証に失敗します。

DBサーバーで実行されたSQL文をログファイルから確認します。

バインド機構によってクライアントから入力されたパスワードの値が適切にエスケープされ、単一引用符が\'となりバインドされていることがわかります。

ITシステムの情報セキュリティ対策でお悩みや課題をお持ちの方いましたらNTT東日本のセキュリティエンジニアにてご相談におこたえしておりますのでぜひお気軽にお問い合わせください。

4. まとめ

今回のコラムではNTT東日本のセキュリティ診断でも検出されるSQLインジェクションについて解説しました。

SQLインジェクションの脆弱性があるとデータベースを不正に利用され、情報の漏えいや改ざん、認証が回避されるといった可能性があります。 そのため、アプリケーションの開発者はSQLを利用する場合、バインド機構(プレースホルダ)を用いたSQL文の組み立てなどの対策が必要となります。

NTT東日本では、最新のセキュリティ情報をリアルタイムで収集・分析し、脅威を素早く検出して防御策を講じるSOCサービスとしてダイヤモンドサポートを展開しています。 また、システムやアプリケーションに存在する潜在的な脆弱性を診断するソリューションも提供しています。 SOC、脆弱性診断に関するご相談は、ぜひNTT東日本へお気軽にお問い合わせください!

ページ上部へ戻る

相談無料!プロが中立的にアドバイスいたします

クラウド・AWS・Azureでお困りの方はお気軽にご相談ください。