このシリーズはPHPでデータベースを扱いたいと考えている方を対象とした記事となっています。
前回までに、PHPのプログラムからデータベースへ接続し、PDO経由でSQLを実行するまでの流れを確認してきました。SQLの基本的なSELECT, INSERT, UPDATE, DELETE文についてもそれぞれ簡単な使い方を紹介してきましたが、今回はデータベースを扱うプログラムで非常に重要なセキュリティーのお話です。
PHPからPDOを利用してデータベースにアクセスする場合、安易な実装をしてしまうとSQLインジェクションという脆弱性を抱えることになります。今回の記事では、SQLインジェクションの解説から、PHP/PDOでの対策までを紹介しています。
データや情報が漏洩するといったニュースを聞くことがあると思いますが、現在のIT社会においては特に「情報の保全」は非常に重要な課題の一つです。情報漏洩やサービスの障害は、長年かけて積み上げてきた貴重なデータ等IT資産の損失だけでなく、サービス提供者の信頼も大きく損ない、その後の事業への悪影響を与えます。
まだデータベースへの接続から基本的なSQLの実行までしか学んでいないのに、もうセキュリティーについて学ぶのかと疑問に思う人もいるかもしれませんが、プログラマーやデータベースエンジニアにとって大事な内容で、基本とセットにして覚えるべきなので、是非確認してみてください。
SQLインジェクションとは
今回紹介するセキュリティー上の欠陥は、SQLインジェクションです。
SQLインジェクションは、データベースシステムを狙った攻撃の一つで、Webサービスなどで「悪意のあるユーザー入力データによって意図しないSQLを実行する」手口・攻撃です。この攻撃は、主にデータの改ざんや不正な情報の取得を目的として行われます。攻撃や手口の事だけでなく、システム上の不備や脆弱性の事も同様に、SQLインジェクションと呼称する場合があります。
実際にデータを不正に入手したり改ざんするには相応の知識などが必要になりますが、脆弱性をついた攻撃をする方法がとても容易なのが特徴で、キーボードなどから入力ができる環境さえあれば、誰でも実行できてしまいます。そのため、利用者が意図せずSQLインジェクションの問題を発生させてしまうことがある程です。
以下もう少し詳しく見ていきましょう。
SQLインジェクションの危険性(脆弱性)
システムの利用者がWebサイトにIDやパスワードを入力するといった画面はよく見かけます。システムはその入力情報を元に、データベースに登録情報があるかを照会する処理を行うことになります。PHP/PDOを使ったシステムを想定し、今回までに紹介した内容を用いて作成された、脆弱性のあるプログラム例を以下に紹介します。
// $dsn, $user, $passwordはデータベースへの接続情報 - 省略
// データベースへ接続
$conn = new PDO($dsn, $user, $password);
// WebのフォームからPOSTされた情報を取得
$web_id = $_POST["user_id"];
$web_pass = $_POST["user_pass"];
// SQL文を準備 (ユーザーが入力した情報を抽出条件に使用)
$sql = "SELECT * FROM user WHERE "
. "user_id ='" . $web_id . "' AND "
. "user_pass = '" . $web_pass . "'";
// Query実行
$conn->query($sql);
SQL文を準備する部分($sql =””)で、ユーザーが入力したIDとパスワード($web_id, $web_pass)をWHERE句に用いて検索条件に使用しています。それぞれデータ型が文字列なので、'(シングルコーテーション)で囲んで指定しています。
この時、ユーザーの入力情報に'(シングルコーテーション)が含まれていた場合どうなるでしょうか。以下に、実際に分断が起こる例 (入力 : $web_id = hoge, $web_pass = Let’s HOGE!)を紹介します。
$sql = "SELECT * FROM user WHERE "
. "user_id = 'hoge' AND "
. "user_pass = 'Let's HOGE!'"
SQL文内の文字列を表す部分が途切れてしまい、その先は文字列ではなくSQL文として解釈されてしまうことになります。また、プログラムの実装次第では、同様に;(セミコロン)が入力されることによって複数のSQLを実行されてしまうという危険性もあります。
このように、ユーザーの入力によってSQLが意図しない形に分断されてしまい、それらがデータベースに発行されてしまうというのが、SQLインジェクションの脆弱性です。
一般の利用者の入力によって発生する場合、上記例のようにSQL文として解釈に失敗する状態になることがほとんどで、その場合はデータベース上でエラーとなって大きな問題になりません。しかし、悪意のある利用者が、データを抜き出したり更新するといった「SQLとして意味のある内容」を入力している場合、プログラムの実装次第ではデータシステム全体が致命的なダメージを受けてしまうことになります。
バインド – SQLインジェクション対策
SQLインジェクションの対策は、「ユーザーの入力に対して適切に処理する」ことが大事です。ただ、ユーザーの入力処理の可能性は無限大で、入力制限などでは対処しつくすことは難しいでしょう。そのため、データベースへの問い合わせを行うSQL生成処理の中で対処をするのが一般的です。この方法は単にバインドと呼ばれる他、パラメータバインドという表現も用いられます。
プログラム上でのバインドの方法は、データベースやその接続に用いる方法で異なりますが、基本的にはユーザーの入力が含まれるSQLを生成する場合に、共通の置き換え処理を通す形で実装します。置き換えを後で行う場所にあらかじめプレースホルダを示し、後で各プレースホルダに値を挿入します。
// 脆弱な例
$sql = "SELECT * FROM user WHERE "
. "user_id ='" . $web_id . "' AND "
. "user_pass = '" . $web_pass . "'";
// プレースホルダ指定に書き換えした例
$sql = "SELECT * FROM user WHERE user_id = ? AND user_pass = ?";
$web_id,$web_passというユーザー入力データがSQL生成時に使われなくなって、代わりに?(クエスチョンマーク)が登場しています。この?(クエスチョンマーク)は、後でプログラム上データを注入する場所を示すため、「プレースホルダ(Place Holder)」と呼ばれます。
$sql = "SELECT * FROM user WHERE user_id = ? AND user_pass = ?";
上記例のプレースホルダの前後には、'(シングルコーテーション)のようなエスケープ文字が含まれていないことが注目ポイントです。こういったプレースホルダに対して値を挿入する方法を使用することで、必要に応じて自動的に付与されます。この処理では、シングルコーテーションに限らず、SQLで使用される本来エスケープが必要なすべての文字種に対して適切に処理されます。
プログラマはこのバインドを行うことによって、ユーザーの入力によって予期しないSQLが発行されてしまうという心配から解放されます。
近年は、ソフトウェアの実装方法についてもコンプライアンス上かなり厳しくなっており、企業内のチェックリストや請負/受託の際の提出資料などでも、実装方法の申告をするリストなどにプログラマが回答するといった場面も多くなっています。このSQLインジェクションの対応が為されているかというのは定番ともいえる基本的なチェック事項で、対応していないと企業によっては納品できない可能性も十分にあるので、データベース技術者やプログラマーの人はしっかり習得しておきましょう。
PDOにおけるバインド – prepareとexecute
ここまでは、一般的なSQLインジェクションと対応策としてのバインドを紹介してきましたが、ここからは本題のPHP/PDOについて紹介していきます。
PDOの中もバインドの方法はいくつか提供されていて、プログラマは自分の望む形で実装することができますが、今回はその中からオススメの方法を一つだけ紹介します。
// データベースへ接続
$conn = new PDO($dsn, $user, $password);
// WebのフォームからPOSTされた情報を取得
$web_id = $_POST["user_id"];
$web_pass = $_POST["user_pass"];
// プレースホルダ指定
$sql = "SELECT * FROM user WHERE user_id = :ph_id AND user_pass = :ph_pass";
// バインド準備
$st = $conn->prepare($sql);
// パラメータ準備
$params = array(':ph_id' => $web_id, ':ph_pass' => $web_pass);
// パラメータバインドしてSQLを実行
$st->execute($params);
$ret_array = $st->fetchAll();
冗長になるエラー処理は全て省いていますが、これでも少し長くて分かりづらいかもしれません。難しそうに見えるかもしれませんが、変更点は多くありません。
全体としては、前回まではPDO::query(バインド無し)を使っていましたが、今回は代わりにPDO::prepareの後にexecute(バインド有り)で実際にSQLを実行しています。対応表にすると以下のような感じです。データベースの値を取り出すfetchAllには変化ありません。
バインド無し | バインド有り |
---|---|
PDO::query | PDO::prepare PDO::execute |
PDOStatement::fetchAll | PDOStatement::fetchAll |
英語でprepareは準備で、executeは実行と、そのままの名前なので覚えやすいかと思います。prepareでプレースホルダ付きのSQLを準備し、executeで実際のパラメータを注入~SQL実行という流れです。パラメータの注入時に、自動的に'(シングルクォーテーション)などのエスケープ処理が行われるため、プログラマはデータ型に関するエスケープ処理を考慮する必要がなくなります。
プレースホルダ[:(コロン) + 名前]と連想配列
上記例では、プレースホルダに見慣れない:(コロン)と名前というのが指定されています。
// プレースホルダ指定
$sql = "SELECT * FROM user WHERE user_id = :ph_id AND user_pass = :ph_pass";
PHPでは、先の説明で紹介したような?(クエスチョンマーク)を用いた疑問符パラメータと、:(コロン)と固有名を付与した名前付きパラメータを、プレースホルダとして利用することができ、今回の例では後者を利用しています。効果は同じですが、その後のプログラムの書き方などが若干変化することになります。
様々な違いがある2つのプレースホルダですが、必ず名前付きパラメータを使用するようにしましょう。
理由としては、特に複数人でプログラムを制作していたり、長い時間メンテナンスしていないプログラムを修正することになった場合などに、疑問符パラメータで作られたプログラムは可読性が著しく乏しく、作業工数の増大につながることが多いためです。疑問符パラメータで実装されていると、?の数を数えながら何番目は~といった余計なプログラム解析の時間を過ごすことになりかねません。
今回は解説のため、データベースの列名(user_id, user_pass)とプレースホルダの名前(ph_name, ph_pass)を別々にした例を紹介していますが、特に意味はありません。パラメータ準備の部分で連想配列を作成していますが、データベースのテーブルと対応したクラスなどを管理している場合などでは、統一することで簡略化できる場面もあるでしょう。
まとめ
今回はPHPを使ったWebアプリケーションの制作において、とても重要なSQLインジェクションの対策について紹介してきました。プログラマーやデータベースのエンジニアは、アプリケーションを制作していく過程の段階から、必ずこの対策が行われている状態であるように心がけ、バインドは「当たり前」という意識になるようにしていきましょう。
バインド無し(query)との使い分け
バインドについて使うのが当たり前になった場合、バインド無しのPDO::queryを使うことがないのかというと、そういう訳でもありません。SQLインジェクションは、ユーザーからの入力がWHERE句などのパラメータとしてSQLに含まれる場合に発生する脆弱性です。そのため、パラメータがそもそもないSQLや、プログラマが内部的に生成したパラメータのみが含まれるSQLなどは、厳密にはバインドの必要性はありません。
ただ、個人的な見解ですが、WHERE句の条件式にPHPの変数が乗るような状況であれば、例えプログラマが設定した値しか通らないとしても、バインドしておくべきと思います。何故なら、後からプログラムを見た別の人が、「脆弱性を発見した」と騒ぎだしたり、プログラマが想定していないデータが通過してSQLインジェクションの問題に発展するといったパターンを「完全に遮断」しておくためです。
パラメータがないSQLはqueryで、パラメータがあるSQLはprepare/executeでという切り分けは、単純ですが効果的でしょう。是非色々試行錯誤してみて、自分なりの最適解を見つけてみてください。