この投稿は
PHP Advent Calendar 2013の13日目の記事です。昨日は
@tanakahisateruの
PHPが糞言語なのはどう考えても参照をポインタだと思っているお前らが悪いでした。
現在twitterのタイムラインで、史上空前のSQLのエスケープブームが起こっています。
これらのうち、最初に参照した大垣さんのブログは、以下のように始まっています。
ツイッターでの議論を見て「SQLエスケープを教える必要はない」とする原因は「教育の基本」と「セキュリティの基本」の理解不足が「根本的な原因」だと解ってきました。
オレオレSQLセキュリティ教育は論理的に破綻しているから引用
これを読んで、SQLインジェクション対策という文脈でエスケープ(SQLの文字列リテラルにおけるエスケープ)を教える必要がないという意見があるのだろうかと疑問に思いました。そのため、PHPの教科書、セキュリティの教科書や冊子(PHPにフォーカスしたもの)合計12種類について、SQLインジェクション対策がどのように説明されているかを調査しましたので報告します。
1. よくわかるPHPの教科書(たにぐちまこと著)
ジャパネットたにぐちさんこと、たにぐちまことさんの書かれたPHP入門書です。長らくPHP入門書のAmazon売り上げトップを独走していました。全くの初心者の状態から、twitter風のひと言掲示板の作成まで進みます。
エスケープの説明 | あり(バックスラッシュのエスケープが抜けている) |
SQLライブラリ | mysql関数 |
対策方針 | エスケープ(mysql_real_escape_string) |
数値列の扱い | sprintfの%d書式+エスケープ |
文字エンコーディング指定 | SET NAMES UTF8 |
上の表のように、基本的にmysql_real_escape_stringによるエスケープ処理でSQLインジェクション対策していますが、数値列に関しては以下のようにsprintfと組み合わせています。
$sql = sprintf('INERT INTO my_items SET maker_id=%d 【略】',
mysql_real_escape_string($_POST['maker_id']));
実は上記は冗長でして、値をシングルクォートで囲っていない場合はエスケープをしても無意味です。上記のSQLインジェクション対策のキモは%d書式による数値化にあります。
これだけであれば、冗長なだけで脆弱性ではありませんが、同書のP272には、sprintfを忘れて単に文字列連結した箇所があり、こちらはmysql_real_escape_string関数によりエスケープ処理はしていますが、脆弱性が混入しています。詳しくは、私のエントリ
『よくわかるPHPの教科書』のSQLインジェクション脆弱性を参照ください。
また、文字エンコーディングの指定をSET NAMES UTF8の呼び出しにより行っていますが、あまり良くありません。その理由については、
libmysqlclientを使うプログラムはset namesをutf8であっても使ってはいけないを参照ください。ただし、大半の環境で脆弱性となることはないと思われます。正しくは、mysql_set_charset関数により指定することですが、mysql関数そのものがPHP5.5から非推奨になりましたので、PDOまたはmysqli関数に移行しましょう。たにぐち本が書かれた頃は、その情報はなかったので、mysql関数を使っていることは仕方ないと思います。
2. いきなりはじめるPHP~ワクワク・ドキドキの入門教室~(谷藤賢一著)
3. 気づけばプロ並みPHP~ショッピングカート作りにチャレンジ!(谷藤賢一著)
「いきなり始める…」は、よくわかるPHPの教科書に代わって、現在Amazonの売り上げトップをひた走るPHP入門書です。HTMLも知らない状態で、教科書に従ってPHPソースを打ち込んでいくと、最終的にDB検索までできるという趣向です。開発環境としてはXAMPPを使用しています。
エスケープの説明 | なし |
SQLライブラリ | PDO |
対策方針 | プレースホルダ |
数値列の扱い | プレースホルダのため考慮の必要がない |
文字エンコーディング指定 | SET NAMES UTF-8 |
ご覧のように、エスケープの説明がありませんが、PDOによるプレースホルダを使って脆弱性が入らないようにしています。PHPの入門書であれば、この方法でも問題ないと感じました。本書の続編に「気づけばプロ並みPHP~ショッピングカート作りにチャレンジ!(谷藤賢一著)」がありますが、SQLに関しては本書と同じスタイルです。本書のサンプルは色々怪しいところがあり、初期化していない配列に要素を挿入していたり(何カ所もある)、session_destroy()を呼ぶ前にsession_start()を呼んでいないためにログアウト処理でセッションが破棄されていない…等々が散見されますが、幸いSQLインジェクション脆弱性はないようです。
4. 基礎からのPHP(西沢 夢路著)
たにぐち本を強く意識していると思われる全ページカラーで、構成もたにぐち本と同様、入門から、画像処理、メール送信、アップロード、DBなどを広範囲に網羅しています。最終的には画像投稿掲示板を作ります。
エスケープの説明 | なし |
SQLライブラリ | PDO(冒頭でmysqlの説明もある) |
対策方針 | プレースホルダ |
数値列の扱い | プレースホルダのため考慮の必要がない |
文字エンコーディング指定 | 指定していない |
本書は最終的にはPDOによりMySQLにアクセスする方法を説明していますが、その前にmysqlを用いて基本的なSQLアクセスを説明しています。この段階のサンプルにはSQLインジェクションがあり、同書P166には下記の言い訳(?)が載っています。
不謹慎ですが、P272で勉強する「SQLインジェクション対策」はまったくしていませんあくまで練習用ということでご了解ください。逆に後でSQLインジェクションを試してみるのも面白いかもしれません。
最初に脆弱性のある状態で説明する方法には同意しませんが、後のPDOのサンプルは、原則としてプレースホルダを用いてSQL文を呼び出しているのでSQLインジェクション脆弱性はありません。ただし、以下の微妙なコードはあります(gz_logon2.php)。
$u = htmlspecialchars($_POST['user'], ENT_QUOTES);
// 略
$ps = $db->query("SELECT pas FROM table2 WHERE id='$u'");
$uはユーザIDですが、HTMLエスケープした状態でSQL文に埋め込んでいます。HTMLエスケープの際にENT_QUOTESを指定しているので、シングルクォートは'とエスケープされるので、SQLインジェクションに使えませんが、バックスラッシュ(円記号)はエスケープされません。しかし、外部から操作できるパラメータが一つだけだとかろうじて攻撃できない気がします。もしもWHERE句が以下の形であれば、SQLインジェクション攻撃が可能です。
WHERE id='$u' and pwd pas='$p' # WHERE句が左記の場合
$u ← \
$p ← or 1=1 #
WHERE句は下記となる
id='\' and pwd pas='or 1=1 #'
~~~~~~~~~~~~~~~
ここまでが文字列リテラル
すなわち、第1パラメータにバックスラッシュを指定すると、第2のパラメータはSQL文の一部となり、UNION SELECTなど好きなSQL機能を用いて攻撃できます。
あぶなっかしい箇所は他にもありますが、それはプレースホルダを使っていないことが原因であり、プレースホルダを使っていれば問題ないものです。
5. かんたんWebプログラミング! これから始める人のPHP学習帖(小川 淳一著)
この本も短いプログラムを打ち込みながら学習するスタイルで、最終的には画像掲示板を作成します。2013年11月30日発行ですから、出たばかりですね。
エスケープの説明 | 間違っている |
SQLライブラリ | mysqli および PDO |
対策方針 | プレースホルダ |
数値列の扱い | プレースホルダのため考慮の必要がない |
文字エンコーディング指定 | mysqli_set_charset およびPDO接続文字列 |
エスケープの説明が間違っているところを引用します(同書P202)。
たとえば、X'masなど、送信されたデータにシングルクォートやダブルクォートが含まれていると、SQL文法エラーとなり、データ登録などの処理が実行されません。必ずhtmlspecialchars関数で無害しておきましょう。
ガタッという感じですが、幸いなことに同書はSQL呼び出しの際に、かたくなにプレースホルダを用いているので、SQLインジェクション脆弱性はなさそうです。多少プログラマの知識が怪しくても(これ自体は残念ですが)、プレースホルダを用いるとSQLインジェクションの可能性をかなり減らすことができる例と言えます。
6. パーフェクトPHP(小川 雄大、柄沢 聡太郎、橋口 誠著)
言わずとしれたパーフェクトPHP。素晴らしい本で、私もPHPの細かい文法を同書で勉強しています。
エスケープの説明 | データベース専用のエスケープ関数を使え |
SQLライブラリ | PDO |
対策方針 | 主にプレースホルダ |
数値列の扱い | プレースホルダの場合は考慮の必要がないが、
エスケープについては記述なし |
文字エンコーディング指定 | なし |
同書のSQL呼び出しの説明は基本的にPDOを用いていますが、SQLインジェクションの説明の節では、プレースホルダが使えない場合はpg_escape_stringやmysql_real_escape_stringなど「データベースエンジン毎に用意された関数」でエスケープするように推奨しています。これは正しい指摘ですが、数値列の場合にどうするかは説明されていません。
7. プロになるための PHPプログラミング入門(星野 香保子著)
タイトルの示すように、入門書を卒業した人がパーフェクトPHPに行く前に読むとよいような位置づけです。入門書だとバリデーションの説明はまずないのですが、同書には「入力値チェック処理」という節が用意されています。CakePHPやAjaxの説明もありと盛りだくさんで、セキュリティの解説も正確です。
エスケープの説明 | なし |
SQLライブラリ | mysqli |
対策方針 | プレースホルダ |
数値列の扱い | プレースホルダのため考慮の必要がない |
文字エンコーディング指定 | set_charsetメソッド |
同書はセキュリティに関しては手堅い解説をしています。SQL呼び出しについても、ライブラリの選択、文字エンコーディングの指定、プレースホルダ利用の徹底などの点で模範的です。
8. PHP逆引きレシピ(鈴木 憲治他著)
これも有名な「逆引きレシピ」です。最近第2版が出ましたが、こちらは第1版のほうです。
エスケープの説明 | データベース専用のエスケープ関数を使うよう説明 |
SQLライブラリ | mysql および mysqli |
対策方針 | プレースホルダを優先し、だめな場合はエスケープ |
数値列の扱い | エスケープの場合はすべての値をシングルクォートで囲む |
文字エンコーディング指定 | mysql_set_charset等 |
第1版のSQLインジェクション対策については、
こちらに批判記事を書きました。問題を要約すると、エスケープすべきでないものまでエスケープしている問題と言えると思います。この問題は第2版では解決されています。
9. PHP逆引きレシピ 第2版(鈴木 憲治他著)
前述の逆引きレシピ第2版です。この第2版は本当に素晴らしい。PHPセキュリティの最新動向をよく把握して、具体的なレシピに落とし込んでいます。すべてのPHP開発者にお勧めします。
エスケープの説明 | データベース専用のエスケープ関数を使うよう説明 |
SQLライブラリ | PDO |
対策方針 | プレースホルダを優先し、だめな場合はエスケープ |
数値列の扱い | プレースホルダの場合は考慮の必要がない。プレースホルダを使わない場合は、「数値以外の文字が混入しないように入力値を検証する必要があります」 |
文字エンコーディング指定 | 接続文字列に指定 |
冒頭に書いたように、この第2版は素晴らしいです。「もうペチパーは緩いなんて言わせない」と叫びたくなるほどのインパクトがあります。たとえば、暗号的に強い乱数が必要な局面では、openssl_random_pseudo_bytes関数を推奨しています。類書だと、uniqidやmt_randを進めている場合が多い状況でした。
SQLインジェクションについては、数値の扱いが厳密になったことと、SQL呼び出しの説明全般がPDOとプレースホルダを用いて記述されていているので安心です。
10. 安全なSQLの呼び出し方
IPAが公開している「
安全なウェブサイトの作り方」の別冊です。SQL呼び出しに特化して詳しく説明しています。
エスケープの説明 | 原理から実際まで詳細に説明 |
SQLライブラリ | PHPサンプルではMDB2 |
対策方針 | プレースホルダ(推奨)、エスケープ |
数値列の扱い | プレースホルダの場合は考慮の必要がない、エスケープの際もquoteメソッドによる数値対応を推奨 |
文字エンコーディング指定 | MDB2の接続文字列 |
本冊子は、SQLインジェクションの前提となるSQL文法の説明、発生原理から、具体的な対策についてまとめたものです。エスケープ処理をしても、あるいはプレースホルダを用いていても、SQLインジェクション脆弱性が混入する原理や、各エスケープ処理用の関数の生成するエスケープ結果を調査して、安全な方法を解説しています。
上記説明のために、エスケープ処理についてはかなり詳細に説明していますが、それは、エスケープ処理を推奨するためではなく、エスケープ処理が難しいものであるので極力避けて欲しいという意図です。そして、原理的に(パラメータ埋め込みの文脈では)SQLインジェクション脆弱性の混入しない静的プレースホルダを推奨し、各言語での具体的な記述方法を解説しています。
また、エスケープ処理については、文字列と数値に分けて、あるべき処理結果と、各ライブラリの実際の挙動を紹介しています。
安全なSQLの呼び出し方は、公開されてから一度も改版されていないので、「安全になった」という表現は適当でありません。現在のものが安全だとすれば、それはもともと安全であったことを意味します。一方、この冊子の本体である「安全なウェブサイトの作り方」は、脆弱性の概要説明であるので、より一般的・抽象的な表現になっていますが、エスケープの説明について第1版から記述があります。以下は第1版のP4からの引用です。
ということで、「IPAの文書」にエスケープが説明されてなかったなんてことはないはずです。
11. 体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践(徳丸浩著)
拙著、いわゆる徳丸本です。
エスケープの説明 | 本文中では簡潔に記して「安全なSQLの呼び出し方」を参照 |
SQLライブラリ | MDB2 |
対策方針 | プレースホルダ |
数値列の扱い | プレースホルダのため考慮の必要がない |
文字エンコーディング指定 | 接続文字列中に記述 |
拙著ではSQL文字列リテラルのエスケープについては簡単にしか説明していませんが、SQLインジェクションの原理を説明する箇所で以下のように説明しています(P128)。
◆ 文字列リテラルの問題
SQLの標準規格では、文字列リテラルはシングルクォートで囲みます。文字列リテラル中にシングルクォートを含めたい場合は、シングルクォートを重ねる決まりです。これを「シングルクォートをエスケープする」と言います。このため、「O'Reilly」をSQLの文字列リテラルにすると、'O''Reilly'になります。
ところが SQLインジェクション脆弱性のあるプログラムでは、シングルクォートを重ねる
処理が抜けているため次のようなSQL文が組み立てられます。
対策に関しては、元々拙著は初心者向けを目指して執筆されたこともあり、複数の方法を示すよりも確実に安全な方法を一つ紹介しようという判断から、実務的な対策方法としてはプレースホルダを主に紹介しています。動的に検索条件やソート列が変化するような「プレースホルダでは書きにくい」と思われてきたクエリについても、プレースホルダでの記述例を紹介しています。
また、SQLインジェクションとは別の問題ですが、LIKE述語のワイルドカード(%と_など)のエスケープが必要なため、こちらのエスケープについては詳しく説明しています。
ということで、拙著においてSQLエスケープは、以下の方針に従っています。
- 原理のところで簡単にエスケープを説明する
- 実務的な対策はプレースホルダ推奨
- どうしてもエスケープが必要な場合は安全なSQLの呼び出し方を読んでね
12. Webアプリセキュリティ対策入門 ~あなたのサイトは大丈夫?(大垣 靖男著)
大垣さんのご著書、いわゆる大垣本です。
エスケープの説明 | あり |
SQLライブラリ | sqlite関数 |
対策方針 | プレースホルダ(推奨)、またはエスケープ |
数値列の扱い | 数値であることの確認 |
文字エンコーディング指定 | なし |
あらためて大垣本を確認して意外に思ったのですが、大垣本もプレースホルダ推奨だったのですね。一度は読んでいたはずですが、大垣さんの近年のエスケープ推しの印象が強いため忘れていたようです。同書P51には概要のまとめとして下記があります。
対策
SQL文のパラメータとなる入力が、数値型である場合は必ず数値型データであることを確認する。文字列型のデータである場合は必ずデータベースシステムにあったエスケープ方式で文字列をエスケープする。プリペアードクエリが利用できる場合は、プリペアードクエリを使用する。
同じくP146には下記の記述があります。
基本的に、データベースが提供する文字列エスケープ関数をすべての変数に使用していれば、脆弱性は発生しません。注意しなければならないのは、データベースによって動作やエスケープしなければならない文字が異なることと、プリペアードクエリのサポートです。
データベースがプリペアードクエリをサポートしている場合は、プリペアードクエリを使用した方がよいです。PostgreSQLはプリペアードクエリが利用できます。
ということで、少なくとも同書が出たころ(2006年3月)は、大垣さんのプレースホルダ(プリペアードクエリ)推しだったようです。今はどうなのでしょうか?
まとめ
PHPの教科書9種類と、セキュリティの解説書3種類を読んで、SQLインジェクション対策と関連してエスケープの説明をしているかどうかを確認しました。
調査前は、私自身は「SQLインジェクションの解説なら当然エスケープは説明するだろう」と予測していたのですが、PHPの教科書ではエスケープを説明していないモノが多く、プレースホルダを用いてSQLインジェクション対策しているものが目立ちました。
一方、セキュリティの解説書の場合は、今回紹介しなかったものも含め、エスケープについては解説していました。
PHPの解説書ではSQLのエスケープを説明しない場合が多い点については、私は、これはこれでアリだろうと思いました。初学者のうちは、とにかく安全に、SQLインジェクションを混入しない書き方が求められます。この点、「プロになるための PHPプログラミング入門」には、SQLインジェクション脆弱性の発生原因として下記が指摘されています。
自分でパラメータを文字列連結しながらSQL文を組み立てている
私は上記に深く同意します。そして、文字列連結なしにSQL文を呼び出そうとすると、必然的にプレースホルダを使わざるを得ません。
また、次の見方もできます。今回調べたPHP入門書の中には、失礼ながら著者自身のプログラミング力量が怪しいものが少なからず見られましたが、その割にはSQLインジェクション脆弱性は見つかりませんでした。これはプレースホルダを使っていれば、(絶対に大丈夫という訳ではないにしても)少々ミスをしてもSQLインジェクション脆弱性の混入は食い止められるということです。このエントリで紹介した唯一のSQLインジェクション例は、エスケープ処理の不適切な実装によるものでした。また、脆弱性ではないにしても、数値列を含むエスケープ処理(あるいはSQLリテラルの正しい構成)は、PHP解説書の著者でさえも難しいらしく、不適切な例が見られました。
全体のまとめは下記の通りです。
- PHP初心者がSQLを呼び出す場合は、「ともかく文字列連結でSQL文を組み立てるな、プレースホルダを使え」という指導は有効
- SQLインジェクションについて深く知るためにはエスケープ処理を知る必要がある
- SQLの動的組み立てを必要とするフレームワーク、O/Rマッパー、データベース管理ツール等の開発には、上記ルールだけでは不足だが、少数の優れた開発者が担当するものであり、必要に応じて高度な技術を学んでもらえばよい
次回は
でこくん(@dekokun)ですね。よろしくお願いします。