2014年12月22日月曜日

『例えば、PHPを避ける』以降PHPはどれだけ安全になったか

この記事はPHPアドベントカレンダー2014の22日目の記事です 。

2002年3月に公開されたIPAの人気コンテンツ「セキュアプログラミング講座」が2007年6月に大幅に更新されました。そして、その一節がPHPerたちを激しく刺激することになります。
(1) プログラミング言語の選択
1) 例えば、PHPを避ける
短時日で素早くサイトを立ち上げることのみに着目するのであれば、PHPは悪い処理系ではない。しかし、これまで多くの脆弱性を生んできた経緯があり、改善が進んでいるとはいえまだ十分堅固とは言えない。
セキュアプログラミング講座(アーカイブ)より引用
「PHPを避ける」とまで言われてしまったわけで、当然ながらネット界隈では炎上を起こし、現在はもう少しマイルドな表現に変わっています(参照)。

本稿では、当時のPHPの状況を振り返る手段として、この後PHPのセキュリティ機能がどのように変化してきたかを説明したいと思います。以下の二点について触れます。
  • PHPの安全でない機能の削除
  • PHPの安全性を高める機能の追加
一方、PHPの単純な脆弱性改修については原則として触れないことにします。
それでは、はじめましょう。

1.htmlspecialchars 文字エンコーディングチェックの改善(PHP5.2.5 2007/11/8)

2007年6月当時のhtmlspecialcharsは第3引数で指定した文字エンコーディングについて、ほとんど何もチェックしていない状態でしたが、PHP5.2.5になって、一部文字エンコーディングのチェックが追加されました。このあたりの詳しい状況は、私のエントリhtmlspecialcharsは不正な文字エンコーディングをどこまでチェックするかを参照ください。PHP5.2.5での対応は、なんとも中途半端なもので、「しないよりはマシだが抜けもあった(参照)」という、なんかPHPの悪いイメージに沿った対応でありました。
しかし、私のエントリが引き金となり、こちらで紹介したような議論が巻き起こり、最終的にはmoriyoshiさんによるとてもきっちりした文字エンコーディングのチェックがなされるようになりました(PHP-5.2.12 2009/12/17 )。
また、PHP-5.3までのhtmlspecialcharsの第3引数のデフォルト値はISO-8859-1(Latin-1)でしたので、文字エンコーディング指定を省略した場合は結局なにもチェックしないのと同じだったのですが、PHP-5.4(2012/3/1)でなんとこれが突然UTF-8に変更されます。これにより、「日本語等マルチバイト環境では第3引数を適切に指定しないと文字が表示されない(参照)」という荒業により、一挙に第3引数の指定が普及したものと思われます。
なんか、昔のPHPのゆるーい感じから、PHP-5.4のこの変更はスパルタンな感じがするほどであります。htmlspecialcharsに関する変遷を下記にまとめます。
  • PHP4.1.0  (2001/12/10) htmlspecialcharsに第3引数追加。ほとんど何もしていないに等しい文字エンコーディングチェック
  • PHP-5.2.5 (2007/11/8) 文字エンコーディングのチェックを強化…したけど抜けがたくさん
  • PHP-5.2.12 (2009/12/17) moriyoshiの神対応による厳格なチェックに
  • PHP-5.4.0 (2012/3/1) 第3引数のデフォルトが UTF-8 に変更

2. register_globalsが非推奨に(PHP-5.3.0 2009/6/30)

PHPの安全でない機能の筆頭格であったregister_globalsは、PHP-4.2.0(2002/4/22)ではデフォルトはオフになったものの、php.iniに指定すれば普通に使える状態でした。PHP-5.3に至り非推奨、すなわち使うと警告エラーとなり、PHP-5.4.0(2012/3/1)にて機能自体が削除されました。

register_globalsの危険な例を紹介します。
session_start();
if (isset($_SESSION['user'])) {
  $islogin = TRUE;
}
ご覧のように、セッション変数userにログインユーザ名を保存することで、ログイン中か否かを保持しています。
ここで、ログイン状態でなくても、クエリ文字列に islogin=1 と指定することでログイン状態になることができます。register_globalの機能により、変数 $islogin の初期値が "1" になるからです。

しかし、この脆弱性は、そもそもログイン状態を保持する変数 $islogin を初期化していないことが原因です。なので、「一見問題ないスクリプト」がregister_globalsのせいで脆弱になる例はないかと探してみました。
仮にスーパーグローバル変数$_SESSIONがregister_globalsによって外部から変更できるとすさまじく危険ですが、それはできないように保護されています。しかし、$_SESSIONが出来る前に使われていた session_register() 関数を使う仕組みだと、セッション変数を外部から設定できる場合があります。そのような例を示します。
session_start();
session_register('user'); // $user をセッション変数として宣言
if (! isset($user)) {
  die('ログインしていません');
}
// 以下ログイン中として処理
コメントにあるように、session_register('user'); は、$userがセッション変数である($_SESSION['user']に相当)と宣言するものです。
しかし、register_globalsが有効だと、新規のセッションの場合に限り、クエリ文字列 user=yamada 等とすることで、セッション変数 $user が外部から変更されてしまいます。PHP-4.1の頃だと、色々大変だったのでしょうね。

また、register_globalsではありませんが、parse_strという関数でregister_globals同等のことを実現しようとすると、$_SESSIONの上書きができてしまいます。詳しくはこちらを参照ください。
register_globalsについてまとめると以下のようになります。
  • PHP-4.2.0 (2002/4/22) register_globalsがデフォルトで off になる
  • PHP-5.3.0 (2009/6/30) register_globalsを有効にすると警告エラーになる
  • PHP-5.4.0 (2012/3/1) register_globalsが廃止される

3. マジッククォートが非推奨に(PHP-5.3.0 2009/6/30)

昔のPHPには、入力値を自動的にSQLエスケープするという機能(マジッククォート)がありましたが、PHP-5.3で非推奨になり、PHP-5.4で廃止されました。
マジッククォートに関しては、PHPの公式マニュアルに妥当な説明があるので参照してください。
上に付け加えることはあまりありませんが、敢えて言えば、
  • マジッククォートは入力時にエスケープ処理を自動的行う仕組みだが、エスケープ処理は文字列を使う時に都度すべきという考え方が一般化した
  • マジッククォートはMySQLに特化したエスケープ方式であり、かつMySQLのオプションや文字エンコーディングを考慮しない不完全なエスケープだった
ということで、廃止になったのは妥当な判断だと考えます。
  • PHP-5.3.0 (2009/6/30) マジッククォートを有効にすると警告エラーになる
  • PHP-5.4.0 (2012/3/1) マジッククォートが廃止される

4.暗号学的に安全な擬似乱数生成器のサポート(PHP-5.3.0 2009/6/30)

昔はPHPに暗号学的に安全な乱数生成関数がサポートされておらず、以下の関数でトークンやらセッションIDなどが生成されるという状況でした。
  • uniqid()
  • rand()
  • mt_rand()
これらは暗号学的に安全な乱数生成器ではないので、セキュリティ用途に使ってはいけません。中でも、uniqid()は乱数ですらなく基本的には時刻を元にしたIDですが、これを追加オプションなしでセッションID生成に使っている実装を見たことがあります。

PHP-5.3.0から、openssl_random_pseudo_bytes という、とても長い名前の関数により安全な擬似乱数が生成できるようになりました。但し、使用にあたっては以下の条件があります。
  • PHP-5.3.0以降であること かつ
  • OpenSSLが導入されていること
どちらか一方でも上記を満たさない場合は、安全な乱数のソースとして/dev/urandom等を使うことになります。

5.セッションID生成の安全性強化(PHP-5.3.2 2010/3/4)

PHPのセッションID生成は、元々暗号的な根拠を持っておらず、PHP-5.3.2で一応の改善があったものの、計算の複雑性を増すことで推測を少し難しくしたというレベルであり、暗号学的な根拠があるものではありませんでした。拙著を書くときにも扱いに苦労した記憶があります。結果として、拙著P163に以下のように書きました。
(PHPのセッションID生成は)図4-51で示したありがちなセッションIDの生成方法に該当します。ロジックの複雑性が高いため解読方法が判明しているわけではありませんが、理論的には安全性が保証されていない設計ということになります。
上記には「解読方法が判明しているわけではありませんが」とありますが、これを調べた人がいて、hnwさんが素晴らしい翻訳で紹介してくださっています。
現実的なリスクは低いものの、ちょっと心配ですよね。このため、(執筆当時は上記論文は知らなかったものの)私は以下のように推奨しました。
しかし、php.iniの設定を追加することで、安全な乱数を元にセッションIDを生成するよう改善できます。そのためには、php.iniに以下を設定します。
[Session]
;; entropy_file は Windowsでは設定不要
session.entropy_file = /dev/urandom
session.entropy_length = 32
そして、PHP-5.4.0からは上記設定が標準のデフォルト値となりました。まさか私の本を読んで、ということはないと思いますが、結果として取り入れてくださると嬉しいですよね。
  • PHP-5.3.2 (2010/3/4) セッションIDの生成方法を複雑化したが不完全
  • PHP-5.4.0 (2012/3/1) セッションIDのシードに安全な乱数を使うように改善

6.ヌルバイト攻撃の防御機能の追加(PHP-5.3.4 2010/12/9)

よく知らているように、PHPの機能・関数にはバイナリセーフのものとそうでないものが混在しているため、ヌルバイト攻撃という攻撃手法が成立していました。
典型的には、ディレクトリトラバーサル攻撃の際に、プログラム側で拡張子を付加してい場合でも任意の拡張子ファイルのアクセスができるようにする方法です。具体的には、../../.../../../etc/passwd%00というファイル名を指定することで、%00(ヌルバイト)が文字列の終端になり、それ以降の拡張子(.txt等)が無視されるというものです(bloggerの制限のため%を全角で書いていますが、実際には半角です)。
PHP-5.3.4では、言語構造及び関数の一部で、ファイル名にヌルバイトがあるとエラーになるように改良されました。詳しくは以下の素晴らしいエントリを参照ください。
ヌルバイト攻撃ができなくなっても、ディレクトリトラバーサル攻撃やファイルインクルード攻撃の対策は引き続き必要ですが、万一対策が漏れていた際の保険的な対策になります。

7.PDOのDB接続時の文字エンコーディング指定が可能に(PHP-5.3.6 2011/3/17)

昔は、PDOを用いる際にDB接続の文字エンコーディングを簡単には指定できないため、SQLインジェクション脆弱性の可能性がありました。具体的には、ぼくがPDOを採用しなかったわけ(Shift_JISによるSQLインジェクション)で指摘したように、Shift_JISでMySQLに接続している場合に、エスケープ処理やプレースホルダを使っていても、SQLインジェクション脆弱性が混入する場合がありました。
PHP-5.3.6以降にて、DB接続時に下図のように文字エンコーディングを指定できるようになりまた。
 $dbh = new PDO('mysql:host=DBHOST;dbname=test;charset=utf8', USERNAME, PASSWORD);
詳しくは以下のエントリを参照ください。
なお、Windows版のPHP-5.3.6には初歩的なバグがあり、PHP-5.3.7で修正されました。
ところが、このPHP-5.3.7には重大な脆弱性が混入してしまいました。次項に続きます。

8. crypt関数の重大な脆弱性混入とXFAILの運用(PHP-5.3.8 2011/8/23)

PHP-5.3.7にはcrypt関数に重大な脆弱性があり、MD5ハッシュを選択した際に肝心のハッシュ値が出力されないという問題が混入してしまいました。バグの詳細と混入の経緯については以下の記事を参照してください。
バグ混入の背景は以下の記事が参考になります。
上記の記事にもありますが、PHPの開発では、バグが採択された場合まずテストを書くことから始めるため、未対応のバグについて、大量のFAILが残ったままになり、対処が必要なFAILが埋もれて見落としてしまったようです。
これに対して、「将来修正するけど今はFAILを容認している」バグについては、FAILとは別に、XFAIL(eXpected FAIL)とすることで、見落としをなくそうということになりました。実際にはXFAILの機能自体は当時から既に組み込んであったようですが、XFAILの運用をきちんとやろうという意味でしょう。
実は、PHP-5.3.7の前にも、PHP-5.2.7にも重大なやらかしがあって、PHP-5.2.7は欠番になっています。そのため、PHP-5.x.7は地雷?ということで、PHP-5.4.7やPHP-5.5.7が出る際にも「また何かあるのではないかと期待不安」があったのですが、さすがにそういうことはありませんでした。PHP-5.3.7の失敗以降、大きなヤラカシはなくなったのではないでしょうか? これは良かったと思いますし、PHP-5.3.7のcryptのバグも幸い大きな実害にはつながらなかったようです。

9. header関数のバグ修正(PHP-5.4.0 2012/3/1)

PHPのヘッダ関数は、5.1.2(2006/1/12)でHTTPヘッダインジェクション対策として以下の修正がありました。
この関数は一度に複数のヘッダを送信できないようになりました。 これは、ヘッダインジェクション攻撃への対策です。
header関数マニュアルより引用
しかしこの修正に抜けがあり、改行を構成する2種類の文字キャリッジリターン(0x0D)とラインフィード(0x0A)のうちラインフィードの方しかチェックしておらず、キャリッジリターンのみでHTTPヘッダインジェクション攻撃ができてしまう状態でした。
その旨を私の本にも書いていたところ、PHP-5.4.0にて廣川類さんが修正してくださいました。
PHPスクリプトに対してHTTPヘッダインジェクション攻撃を掛ける経路としては、header関数のほか、setcookieとsetrawcookieを使う可能性が考えられますが、この修正でいずれの関数でもHTTPヘッダインジェクションはできなくなったはずです。

10. 安全なパスワード保存が簡単にできるようになった(PHP-5.5.0 2013/6/20)

password_hash関数の新設により安全なパスワード保存が楽に行えるようになりました。以下のように使用します。
$password = ...
$hash = password_hash($password, PASSWORD_DEFAULT);
生成されるハッシュ値の例は下記のとおりです。パスワードが同じでもソルトが毎回変わるので、呼び出しの度に異なるハッシュ値になります。
$2y$10$KDeRvOFZVPtVVm/Qo8DhA.XZ85u9mPmIqj3CHXCD2QZxeop617Wy2
上記の呼び出し例では指定していませんが、ストレッチングの強度を指定することもできます。デフォルトは10(ストレッチ回数ではありません)です。
パスワードの悪用が問題になっている昨今の状況を考えると、password_hashの採用はぜひ検討したいものです。

11. Session Adoption Bugの修正(PHP-5.5.2 2013/8/15)

PHPには従来からセッションIDとして勝手な値(例: PHPSESSID=ABC)をつけてもそれを受け入れてしまうという問題(Session Adoption)が指摘されていました。PHP開発陣はSession Adoptionはない方がいいが重大な問題ではないと認識していたようで長らく放置されていましたが、PHP-5.5.2にて修正されました。ただし、デフォルトは従来通りで、php.iniにて session.use_strict_mode = On を指定する(strict sessions)と、PHP側で用意したセッションIDのみを使うようになります。
詳しくは以下のエントリを参照ください。
また、Session Adoptionに関連して、セッションIDの固定化攻撃にどう対処するかについては、以下を御覧ください。

まとめ

IPAから『例えば、PHPを避ける』と書かれた2007年6月以降、PHPの安全性がどの程度強化されたかを調べました。思い返すと色々あったなぁとは思うものの、PHP-5.3.7のcryptの以降は、(PHP-5.5.2のStrict Sessionsはご愛嬌として)大きな「やらかし」は発生していないように思います。
また、PHP-5.3以降では、明確に過去の悪い習慣と決別する姿勢を示しているように思えます。昔のイメージだけで「PHPは危険」と言われてしまうのは、ちとかわいそうな気がします。
ということで、最近のPHPの安全性はかなり向上していると言えますが、むしろ、現在のPHPのセキュリティ運用上の問題は、バージョンアップが頻繁なために追随することが難しいことではないでしょうか? これを避けるためには、CentOS等のLinuxディストリビューションのパッケージとしてPHPを導入して、適切にパッチ適用する方法などが考えられます。

0 件のコメント:

コメントを投稿

フォロワー