プライバシーポリシー

2011年12月30日金曜日

Webアプリケーションに対する広範なDoS攻撃手法(hashdos)の影響と対策

28C3(28th Chaos Communication Congress)において、Effective Denial of Service attacks against web application platforms(Webプラットフォームに対する効果的なサービス妨害攻撃)と題する発表がありました(タイムスケジュール講演スライド)。

これによると、PHPをはじめとする多くのWebアプリケーション開発プラットフォームに対して、CPU資源を枯渇させるサービス妨害攻撃(DoS攻撃)が可能な手法が見つかったということです。この攻撃は、hashdos と呼ばれています。

概要

PHPなど多くの言語では、文字列をキーとする配列(連想配列、ハッシュ)が用意されており、HTTPリクエストのパラメータも連想配列の形で提供されます。PHPの場合、$_GET、$_POSTなどです。
連想配列の実装には、高速な検索が要求されるためハッシュテーブルが用いられます。ハッシュテーブルは、文字列を整数値(ハッシュ値)に変換するハッシュ関数を用いて、平均的には一定時間に検索・挿入・削除が行えるデータ構造です。しかし、ハッシュ値が一致する(衝突する)キー文字列については、通常ハッシュテーブルは順次的な探索となり、検索・挿入などが遅くなります。
hashdosは、ハッシュ値が同じになるキーを多数POSTパラメータに含ませることにより、CPU資源を枯渇させる攻撃です。cryptanalysisによると、300k、500k、8MBのデータを用いた処理時間は以下の通りだそうです。
  • 8 MB of POST data -  288 minutes of CPU time
  • 500k of POST data - 1 minute of CPU time
  • 300k of POST data - 30 sec of CPU time
PHP向けののPoCが公開されていたので試したところ、約1.4MBのPOSTデータ(パラメータ数65536)に対して、6分弱掛かり、その間CPUは99%消費していました。私はBurp SuiteのRepeater機能で試しましたが、wget等でも実行できるでしょう。極めてカジュアルで効果的なDoSと言えます。

影響を受けるプラットフォーム

Perl以外の多くの言語で影響があります。Java(Apache Tomcat7.0.22以下、同6.0.34以下、同5.5.34以下) 、PHP、ASP.NET、Python、Ruby(1.8.7-p356以下のみ)などです。詳しくは、cryptanalysisの解説を参照して下さい。

影響

DoS攻撃の最中はCPU資源が攻撃に割り振られてしまい、サービスの提供が困難になると考えられます。
Apache Killerと異なり、メモリ資源が枯渇するわけではないので、攻撃がやめば直ちにサービスは回復するようです。

解決策、回避策

パッチが提供されている言語については、パッチを速やかに適用して下さい。
ASP.NETについては、MS11-100のセキュリティパッチにて修正されています。
Apache Tomcatについては、7.0および6.0については対策バージョンが出ているので、速やかにバージョンアップしてください(2012/1/6追記)。Tomcat5.5についても対策バージョン(5.5.35)が出ました(2012/1/19追記)。
PHPについては、PHP5.3.9にて修正される予定です。PHP5.3.9RC5-devおよびPHP5.4.0RC4にて修正済みであることを確認しました。PHPの修正は、受け付けるパラメータ数の上限を規定するもので、php.iniのmax_input_varsとして指定できます。デフォルト値は1000ですが、必要最小限の値を指定することで安全性が高まります。PHPの修正は、パラメータ数を制限するものですので、根本的解決とは言えませんが、現実的には、パラメータが1000以上必要というアプリケーションはまずないと思われるので、現実的な対応としてはありだと思います。

PHP5.3.9が出るまでの間は、回避策として、php.iniで以下を制限するとよいでしょう。
  • post_max_size(POSTパラメータの最大バイト数; デフォルトは8MB)
  • max_input_time(入力処理の最大時間; デフォルトは-1(無制限))
アップロード機能のないアプリケーションであれば、post_max_sizeをかなり小さくすることで、hashdos攻撃の影響を事実上なくすことができます。
一方、max_input_timeを短く設定すると、たとえ攻撃にあっても、攻撃の影響を緩和できます。max_input_timeは、「サーバがすべてのデータを受け取ってからスクリプトの実行を開始するまでの時間」ということで、hashdosの実行時間(POSTデータを$_POSTにセットする時間)と関連します。
これらの回避策は、根本的対策ではありません。とくに上記パラメータを絞り込みできないアプリケーションでは、できるだけ早期にPHP5.3.9への移行を推奨します。
あるいは、WAFによりパラメータ数を制限できれば、有望な対策となります。

追記(2011/12/30 23:20)

mod_securityによる対策が有効であることを確認しました。mod_securityの最新版(2.6.3)およびCRS(Core Rule Set)の最新版(2.2.3)にて確認しました。
mod_securityのルールとしては、以下の2つが効いています。

  • SecRequestBodyNoFilesLimit
  • modsecurity_crs_23_request_limits.conf の Maximum number of arguments in request limited

前者は、ファイル以外のPOSTのボディサイズ制限で、デフォルトは1MBです。
後者は、パラメータの個数を制限するルールで、デフォルトは255です。

いずれも、「SecRequestBodyAccess On」を設定しておかないと有効にならないと思います。これは、POSTパラメータをmod_securityでチェックするという指定です。

【参考】mod_securityの導入方法をブログに書きました


また、Ruby1.9は対象でなく、Ruby 1.8.7-p356以下が対象となります。Ruby 1.8.7-p357以降、1.9.xでは対応済みです。

追記(2011/12/31 12:30)

28C3の講演資料によると、PHPのsuhosinパッチが回避策として挙げられています。未検証だったので紹介していませんでしたが、こちらも検証してみました。
Ubuntuの場合、パッケージでPHP5を導入している場合は、suhosinパッチを簡単に導入できます。
$sudo apt-get install php5-suhosin
/etc/php5/apache2/conf.d/suhosin.ini というファイルが作られますので、このファイルによりsuhosinの設定が可能です。
hashdosに効果があるのは以下のパラメータです。
;suhosin.post.max_vars = 1000
;suhosin.request.max_vars = 1000
それぞれ、POSTパラメータ数とリクエストのパラメータ総数を制限するものです。制限する場合、エラーにするのではなく、超過分を無視するようです。
どちらもデフォルトは1000となっています。この程度の値でも、hashdosには相当の効果が見込めます。suhosinの場合、先のPoCで6分弱掛かった際のパラメータ数は65536でした。これが1/65になるわけですが、hashdosの実行時間はパラメータ数に2乗に比例しますので、
6 * 60 / 65 / 65 = 0.085
と、0.1秒未満になる計算です。通常はパラメータ数が1000近くなることはないと思いますので、さらに絞り込むと安全性が高まります。その場合、行頭のコメント記号「;」を削除することをお忘れなく。
mod_securityの新規導入は大変ですし、PHP5.3.9もまだリリースされてないようですので、PHPアプリケーションをhashdosから防御するには、当面suhosinが有効だと思います。ただし、suhosinの導入による副作用も考えられますので、まずはテスト環境でテストしてから本番環境に適用することをお勧めします。

また、Apacheの設定で簡易的な対策をとることも可能です。以下はPOSTのボディサイズを100K以下に制限する指定です。Apacheのマニュアルに紹介されている例です。
LimitRequestBody 102400
この方法は、Apacheを使っている環境では、言語を問わず有効です。
但し、大容量ファイルのアップロード機能があるようなアプリケーションでは、この対策は難しいでしょう。

まとめ

hashdosについて報告しました。hashdosの特徴は簡単(POSTリクエスト一発)でCPUを枯渇させることができるという意味で、カジュアルなDoS攻撃ができることが特徴です。一方、メモリ資源は枯渇しないことから、ApacheKillerほどの破壊的な攻撃ではないという印象を受けました。
年末・年始にかけて迅速な対応ができない職場も多いと思いますが、できるだけ早期に回避策を導入すること、Webサイトの監視を強化すること、hashdosによる攻撃の情報に注意することを推奨します。

[PR]
安全なWebアプリケーションの作り方DRMフリーのPDFによる電子版もあります。

2011年11月29日火曜日

KDDIの新GWで「かんたんログイン」なりすましの危険性あり直ちに対策された

au/KDDIの2011年秋冬モデル(現時点ではF001のみ)にてEZwebとPCサイトビューア(以下PCSV)のゲートウェイが統合されたことに伴い、かんたんログインを実装しているサイトに対して、F001のPCSVからJavaScriptを用いた「なりすまし」攻撃ができる状態でした。この問題をKDDIに通報したところ、直ちに対策が取られ、現在は安全な状態です。以下、詳しく報告します。

目次


概要

以前、「EZwebの2011年秋冬モデル以降の変更内容とセキュリティ上の注意点」にて、auの2011秋冬モデルにてEZwebとPCサイトビューア(以下PCSV)のゲートウェイが統合される(以下「新EZサーバー」と表記)ことによるセキュリティ上の懸念と、Cookieの仕様変更について予測しました。両者は結果として「アタリ」でした。本稿では、セキュリティ上の問題について報告します。なお、セキュリティ上の問題はKDDIに報告し、既に解消しています。

経緯

経緯を時系列で示します。全て2011年です。
  • 9月28日(水):秋冬モデルよりEZwebとPCSVのIPアドレスが統一されると発表(魚拓)
  • 11月9日(水):F001が関西など一部地域で発売開始。関東は11月10日発売開始。
  • 11月10日(木):徳丸がF001を購入。検証を開始するも、この日のうちには問題を発見できず。
  • 11月11日(金):早朝、OpenPNEの海老原昂輔(@co3k)さんから、PCSVにてEZ番号を偽装できる旨の連絡と、OpenPNEの対応と情報公開の相談を受ける。KDDIに連絡して対処してもらうことを優先し、公表をしばらく見合わせることを合意
  • 11月11日(金):ツテを通じてKDDIに連絡
  • 11月12日(土):F001のPCSVのIPアドレスが変更されていることを確認。成りすましの危機は回避される。この状態のまま現在に至る
  • 11月28日(月):EZfactoryにて「※ セキュリティ確保のため、EZブラウザとPCサイトビューアーは、従来通り異なるIPアドレス帯域を使用しております」と発表(直リンク)(魚拓)

ということで、連絡の翌日には回避策がとられました。迅速な対応だと思います。

何が問題か

ここで扱っている問題は、PCSVのJavaScriptを悪用して、ケータイWebサイトの「かんたんログイン」なりすましの可能性です。基本的には、以下のエントリで説明した内容です。

携帯JavaScriptとXSSの組み合わせによる「かんたんログイン」なりすましの可能性

このレポートではXSSによる攻撃のみに言及していますが、その後の研究により、DNSリバインディング攻撃を使っても攻撃が可能であることが分かっています。もっとも、ケータイブラウザのJavaScriptでは、XMLHttpRequestオブジェクト(以下XHR)によるHTTPリクエストヘッダを変更に大幅な制限がかけられたため、この攻撃は成立しません。一方、XHRに大きな制限をかけたことで、ケータイJavaScriptはインターネット一般のJavaScriptとの互換性が大きく損なわれました。
今回の問題は、ケータイブラウザ(iモード、EZweb、Yahoo!ケータイのブラウザ)ではなく、PCSVを使った攻撃であることが、従来とは異なります。PCSVからの攻撃が可能になった理由は、F001以降のEZweb端末で、EZブラウザとPCSVの使うゲートウェイが共通になったためです。つまり、かんたんログインの前提として、ケータイブラウザからのリクエストであることを確実にチェックする方法として、リモートIPアドレスを用いていたのに、その同じIPアドレスにPCSVからのリクエストが混じるようになったためです。

経緯説明(1)基本的なチェックは対処済みだった


KDDIは、これによる危険性を承知していたようで、F001のPCSVのXHRには以下の処置が施されていました。
  • リクエストヘッダX-UP-SUBNO(EZ番号)が追加できない
  • リクエストヘッダUser-Agentが変更できない

これらにより、PCSVを使ってもEZ番号の詐称ができないよう配慮されていました。User-Agentの変更もできないようにしている理由は、EZwebかPCSVの区別をつけるためと、au以外の事業者の端末に成りすましができないようにとの配慮でしょう。

経緯説明(2)ハイフンをアンダースコアに変えるトリックは対策済み

CGIやPHP等一部の言語では、foo-barという形のリクエストヘッダは、HTTP_FOO_BARという形式で受け取ることになります。これは、CGIの仕様として、リクエストヘッダを環境変数にして受け渡す際の約束です(下表)。PHPは、CGIの形式を踏襲しています。

HTTPリクエストヘッダ環境変数として受け取る形式
foo-barHTTP_FOO_BAR
User-AgentHTTP_USER_AGENT
X-UP-SUBNOHTTP_X_UP_SUBNO

一方、Java EE(Servlet)(HttpServletRequest#getHeaderメソッド)や.NET(Request.Headersプロパティ)では、リクエストヘッダを元のまま受け取ります。以下、CGIの形式の場合について議論します。
CGI等では、X-UP-SUBNO(EZ番号)はHTTP_X_UP_SUBNOというキーで受け取るので、最初からX_UP_SUBNOというリクエストヘッダにすれば、CGIプログラムやPHPスクリプト側ではEZ番号として受け取る可能性があります。

HTTPリクエストヘッダ環境変数として受け取る形式
X_UP_SUBNOHTTP_X_UP_SUBNO

この方法については以前テストしたことがあり、WAS Forum 2010で発表しました。このスライドの54ページ以降を参照下さい。ただし、このトリックは、ソフトバンクの一部端末でかつEnd-to-EndのSSLの場合のみ有効で、平文通信の場合はゲートウェイ側でチェックされているようです。これらの事実から、SSLではかんたんログインを受け付けるなという結論になります。
F001でもこのトリックが使えないかと思い、購入直後からチェックしましたが、ハイフンをアンダースコアに変更しても、EZ番号やUser-Agentに相当するヘッダは送信できませんでした。
アンダースコアがダメ(ちゃんとチェックされていた)だったので、ヌルバイトや改行など、過去問題になったことのある手法を試してもチェックをできなかったので、その日(11月10日)はあきらめて寝てしまいました。

経緯説明(3)海老原氏が発見したトリックとは

ところが、その日の深夜に海老原さんからメールが入っていました。メールによると、X.Up.Subnoというヘッダを送る(ハイフンやアンダースコアではなくドットを使う)ことで、PHPスクリプトからはHTTP_X_UP_SUBNOとして受け取れるというのです。確認してみると、CGIプログラムおよびPHPスクリプトでは確かにそうなります。

海老原さんからの相談は、既にOpenPNEはF001のIPアドレスを有効にしてかんたんログインが可能にする対応を配布済みだが、上記トリックにより成りすましができる(可能性がある…後述)。リストを取り下げると、その理由も説明しなければならず、他のサイトに対して攻撃されるリスクがあるので悩ましい、というものでした。
これに対して、徳丸からは、まずはKDDIに通報して、速やかに対処されればそれでよし、遅くなるようであればあらためて公開方法を考えようとアドバイスしました。海老原さんも合意され、徳丸からKDDIに連絡することになりました。

経緯説明(4)KDDIに連絡→翌日に対処

徳丸からは、信頼できる方を通じてKDDIの該当部署にピンポイントに連絡を取りました。至急動いて下さるという返事を頂いたので、しばらく待つことにしまして、その旨を海老原さんにも伝えました。
すると、翌日になって変化がありました。PCSVからのリクエストのリモートIPアドレスが変化しました。

11月12日朝までのIPアドレス:111.107.116.*
11月12日夜以降のIPアドレス:111.87.241.*

すなわち、11月12日の朝までは、EZfactoryにIPアドレス帯域として記載されたIPアドレスからリクエストが来ていましたが、同日の夜(21:55頃最初観測)以降は、別のIPアドレスにPCSVが移されたことになります。
PCSVからのリクエストが別IPになれば、PCSVを用いた「かんたんログイン」の成りすましはできなくなります。

実証例

検証用の比較的シンプルなスクリプトを以下に示します。User-Agentは実機そのまま、EZ番号は、0509999…で始まる架空のものにしてあります。

検証用HTML
---------------------
<html>
<head>
<script>
function test()
{
    var requester = new XMLHttpRequest();
    requester.open('GET', 'dump.php', true);
    requester.onreadystatechange = function() {
       if (requester.readyState == 4) {
           onloaded(requester);
       }
    };
    requester.setRequestHeader("User.Agent", "KDDI-FJ31 UP.Browser/6.2_7.2.7.1.K.8.160 (GUI) MMP/2.0");
    requester.setRequestHeader("X.UP.SUBNO", "05099999999999_vi.ezweb.ne.jp");
    requester.send(null);
}

function onloaded(requester)
{
    res = requester.responseText;
    document.getElementById('result').innerHTML = res;
}
</script>
</head>
<body onload="test()">
<div id="result"></div>
</body>
</html>

dump.php
---------------------
<?php
  echo "UA:" . htmlspecialchars($_SERVER['HTTP_USER_AGENT']) . "<br>";
  echo "EZNO:" . htmlspecialchars($_SERVER['HTTP_X_UP_SUBNO']);
?>

F001のPCSVで実行した結果の画面を下図に示します。User-AgentとEZ番号が偽装されている様子が分かります。



外部からJavaScriptを実行できる条件

この節は技術的に誤りでした。海老原さんの日記によると、F001のPCSVはアドレスバーからのJavaScript実行が可能なので、全てのサイトで、任意のJavaScriptを利用者が実行できるそうです。
F001のPCSVからJavaScriptで攻撃を行う場合、JavaScriptの同一生成元ポリシーを回避しなければ、攻撃対象サイトでJavaScriptを実行することができません。
この同一生成元ポリシー回避として、よく使われるテクニックは、クロスサイト・スクリプティング(XSS)です。攻撃対象サイトに1箇所でもXSS脆弱性があれば、それを悪用してJavaScriptを実行することができます。
もう一つの方法は、DNSリバインディング攻撃です。ケータイのDNSリバインディング攻撃については、「iモードIDを用いた「かんたんログイン」のDNS Rebinding脆弱性」を参照下さい。この問題に対して、対策を施しているサイトはDNSリバインディング攻撃によるJavaScript実行はされません。F001のPCSVでは、Hostヘッダの書き換えはできません。

影響を受けるサイトの条件

この問題の影響を受けるサイトは以下の全ての条件を満たすサイトです。
  • かんたんログインを実装している
  • 新EZサーバーのIPアドレスを許可している
  • ヘッダ名の記号をアンダースコアに変更する言語を使っている(CGIやPHPなど)
  • JavaScriptを外部から実行する手段がある(前項参照)

これらの条件を全て満たすサイトは、かなり存在する(存在した)と予想しています。


影響

前項の条件を満たすサイトでは、以下の影響があります。

  • 利用者の介在なしに、任意の利用者へのなりすましが可能

ただし、利用者のケータイIDは既知であるとします。
ケータイIDの収集は容易です。攻撃に先立ち、ケータイ向けWebサイトを開設して利用者を集めるだけで、閲覧者のケータイIDを収集することができます。また、ランダムに生成したケータイIDを攻撃に用いることも可能です。

サイトの実装によっては、auの利用者だけでなく、NTTドコモやソフトバンクの利用者に成りすましできる場合があります。その条件は、リモートIPアドレスによってキャリア判定していない場合です。言い換えれば、User-AgentやケータイIDによってキャリア判定している場合は、これらヘッダがJavaScriptにより改変可能なので、NTTドコモやソフトバンクの端末に成りすまし可能です。

ところで、この攻撃の前提として、WebサイトにXSS脆弱性かDNSリバインディング攻撃の対策をしていないという条件がつきます。このため、攻撃を受けるサイトは、元々脆弱なので、新たな脅威は生まれないのではないか、と思う読者もいると思います。
しかし、XSSやDNSリバインディング攻撃は受動的攻撃であり、罠を閲覧した利用者のみが影響を受けるのに対して、ケータイIDの変更による成りすまし攻撃は、利用者の介在なしに任意の利用者に成りすましできる点が異なります。すなわち、この問題により非常に多数の利用者が影響を受ける点が問題です。
ひょっとすると、従来XSSやDNSリバインディング攻撃の脅威を知りつつも、「影響が小さいので対策しない」という判断を(好ましくはないですが)しているサイトもあるかもしれません。そのようなサイトに対しても大きな脅威が生まれます。


対策

PCSVによるかんたんログイン成りすましは、既にKDDIにより対策されていますが、もし対策されていないとすると、以下が対策になります(なりました)。

  • XSS脆弱性をすべてなくす かつ
  • DNSリバインディング攻撃対策をする(ホスト名のチェックなど)

これらは、勝手にJavaScriptを実行されないようにする対策です。今回の問題がなくても、上記は必須です。

※追記:今回の問題に対して効果がないので取り消しましたが、上記が元々必須であることには変わりません。

加えて、以下を実施すると良いでしょう。
  • HTTPリクエストヘッダを生のまま取得できる関数を使用する

Java Servlet(Java EE)や.NETでは、これは元々実現されています。
PHPを使う場合は、getallheaders()関数を使うと、生のHTTPリクエストヘッダを取得できます。
これらにより、X.Up.Subnoと指定したヘッダは、X.Up.Subnoのまま取得されることになります。HTTP_X_UP_SUBNOにはなりません。

さらに、かんたんログインの安全性を保つための必要条件として以下があります。これは、「間違いだらけの「かんたんログイン」実装法」を@ITに寄稿した際にまとめたものですが、読者の便宜のために再掲します。DNSリバインディング攻撃対策については重複しています。
  • 携帯電話事業者のゲートウェイからのアクセスのみを受け付ける(IPアドレスチェック)
  • リモートIPアドレスを基にキャリア判定を行う(User-AgentなどHTTPリクエストヘッダで判定してはいけない)
  • 契約者固有IDとしてはiモードID、EZ番号、ユーザーID(ソフトバンク)のみを用いる
  • HTTPSではかんたんログインを受け付けない
  • Hostヘッダのチェックまたは名前ベースのバーチャルホストを設定する
  • ソフトバンクの非公式JavaScript対応端末へ何らかの対処を行う

非常に多くの条件ですが、これらで本当に安全になるかは誰にも分かりません。そのため、この機に、かんたんログインをやめ、パスワード認証等に移行することを真剣に検討しましょう。

今回の問題は、端末あるいはau設備の脆弱性なのか

さて、これまで説明したF001のPCSVの問題は、端末ないしau設備の脆弱性なのでしょうか。
私は、KDDIが責任を持つべき脆弱性ではないと考えています。KDDIは、かんたんログイン手法に対して、なんら保証をしていないからです。
とはいえ、完全に「シロ」と言い難い面もあります。
KDDI au: そのほかの技術情報 > HTTP Requestヘッダ」には、以下の記述があります。
※2011年秋冬モデル以降の一部機種ではPCSVとEZブラウザのIPアドレス帯域は統合されますが、例えば、ユーザーエージェントを組み合わせることでブラウザを判別することができます。

また、同じページから参照されているユーザエージェントの「判定CGI(サンプル)」は、PCSVからの「成りすまし」に対して、誤判定します。
しかしながら、これらはあくまでもミクロの問題です。同じページには以下の記載があります。
EZ番号とは、EZweb契約ごとにユニークに付与されるIDです。例えば、ユーザーの判別などに利用することができます。

※IPアドレス/ユーザーエージェントでのフィルタ、ID/パスワード認証を併用するなど、サイト内容に応じた適切な方法でご利用ください。

かつては、同じページ内に「ユーザ認証に用いる場合には…」という記載があり、EZ番号による認証をキャリアとして認めているように読める文面でした(参考:「EZ番号に関する注意書きが変更された」)。それが「ユーザの判別」という曖昧な表現に変更され、かつ「ID/パスワード認証を併用する」という注記が追加されたことから、KDDIはEZ番号によるかんたんログインに対して保証を与えていないと私は解釈します。

しかし、大手著名サイトを含む多くのサイトがかんたんログインを実装していることもまた事実です。この影響を考慮し、KDDIはPCSVのIPアドレスを分けるという対処をしたのだと思います。この対応は妥当だったと私は考えます。

まとめ

au/KDDIの2011年秋冬モデルF001のPCSVを用いたかんたんログインなりすまし問題について説明しました。一時的に、多数のユーザに対する成りすまし可能な状態がありましたが、KDDIにより早期に対処され、現在では安全と思われます。しかし、この機に、かんたんログインが極めて危うい状況にあることをご認識いただき、かんたんログインを廃止することを強く推奨します。

[PR]
安全なWebアプリケーションの作り方DRMフリーのPDFによる電子版もあります。

2011年11月7日月曜日

PHP5.4のhtmlspecialcharsに非互換問題

PHP5.4.0から、htmlspecialchars関数のデフォルト文字エンコーディングがISO-8859-1(Latin-1)からUTF-8に変更されます。これに伴い、従来動いていたアプリケーションが動かなくなるケースが出てきます。典型的には、以下の両方の条件に該当するアプリケーションは、マルチバイト文字が表示されなくなります。

  • 内部文字エンコーディングとしてEUC-JPまたはShift_JISを用いている
  • htmlspecialcharsの第3引数を指定していない

htmlspecialchars関数の第3引数の変更内容
htmlspecialchars関数の第3引数には、処理対象文字列の文字エンコーディングを指定します。この指定をしない場合、従来(PHP5.3まで)はISO-8859-1とみなされていたのに対して、PHP5.4ではUTF-8とみなされるようになります。
また、第3引数として空文字列('')を指定した場合mbstring.internal_encodingで指定した文字エンコーディングが指定されることになっています。この仕様はPHP5.4でも変わりませんが、細かい挙動が変わります。これらを表としてまとめました。


PHP5.3まで
第3引数意味
何も指定しないISO-8859-1
空文字列''を指定mbstring.internal_encodingに従う
文字エンコーディングを明示明示された文字エンコーディング

PHP5.4以降
第3引数意味
何も指定しないUTF-8
空文字列''を指定mbstring.internal_encodingに従う (注意点を後述)
文字エンコーディングを明示明示された文字エンコーディング


第3引数を指定していない場合の影響

前述のように、htmlspecialchars関数の第3引数を指定していない場合、PHP5.3までは、文字エンコーディングがISO-8859-1が指定されたとみなされます。この場合、入力内容にかかわらず不正な文字エンコーディングと判定されることはありません。したがって、文字エンコーディングのチェックが働かない代わりに、エラーになることもありませんでした。
これに対して、PHP5.4の仕様により文字エンコーディングがUTF-8とみなされた場合に、Shift_JISやEUC-JPの2バイト文字が入力されると、高い確率で「UTF-8として不正」というエラーになり、htmlspecialchars関数の出力は空になります。つまり、プログラムが正常に動作しません。

  • htmlspecialchars関数の第3引数を指定しておらず、内部文字エンコーディングがShift_JISあるいはEUC-JPの場合、プログラムが正常に動かなくなる

これの簡単な対処法がないが探しましたが、簡単な方法はないようです。以下の3つを考えました。

  • プログラムを修正してhtmlspecialchars関数の第3引数を明示する
  • 内部文字エンコーディングをUTF-8に変更する
  • PHPのソースにパッチをあてて、アプリケーションの文字エンコーディングがデフォルトになるようにする

大規模なアプリケーションの場合、上の2つは一筋縄ではいかない感じですね(htmlspecialcharsの呼び出し箇所が多いため)。PHPにパッチをあてることを推奨するものではありませんが、現実的な選択肢として検討したくなるプロジェクトもありそうです。それ以前に、PHP5.4に移行しないという選択になりそうですが、PHP5.3のメンテナンスがいつまで続くかは不透明なので、PHP5.4への以降は計画しておかなければならないと思います。

まとめてhtmlspecialchars関数を修正する方法

htmlspecialchars関数を一括して修正する方法として、以下の方法があります。アプリケーション中のhtmlspecialcharsを全てhxなど別名に変換します。hxという名前がアプリケーション中で使われていれば、別の名前にします。
次に、関数hxを以下のように定義します。アプリケーションの文字エンコーディングはEUC-JPと仮定します。Shift_JIS等の場合は適宜変更して下さい。

function hx($str, $flags = ENT_COMPAT, $charset = 'EUC-JP') {
  return htmlspecialchars($str, $flags, $charset);
}

この方法により、比較的楽に、安全に文字エンコーディングを指定できることになります。

PHPそのものにパッチをあてる

あまりお勧めしませんが、PHPにパッチをあてて、htmlspecialcharsの第3引数を省略した際の挙動を、mbstring.internal_encodingに従うよう変更するというアイデアもあります。
パッチの案を以下に示します。

--- html.c.org  2011-11-06 16:29:18.438601385 +0900
+++ html.c      2011-11-06 16:29:52.608727050 +0900
@@ -368,11 +368,7 @@
        int len = 0;
        const zend_encoding *zenc;

-       /* Default is now UTF-8 */
-       if (charset_hint == NULL)
-               return cs_utf_8;
-
-       if ((len = strlen(charset_hint)) != 0) {
+       if (charset_hint != NULL && (len = strlen(charset_hint)) != 0) {
                goto det_charset;
        }

ご覧のように、元のソースは文字エンコーディングが指定されていない場合、UTF-8を返すよう力強くハードコーディングされていますが、パッチではこの処理を削除し、文字エンコーディングが空文字列の場合と同じ処理になるようにしています。あまりテストはしていないので、もし採用される場合は、十分なテストをしてから使って下さい。


PHPの教科書はどうか

従来、PHPの教科書には、「htmlspecialcharsの第3引数は指定しなくてもよい」という説明が多かったようです。試みに、手元のPHPの教科書を調べたところ、以下の5つは、htmlspecialcharsの第3引数を指定せずに説明しています。

これらのうち、逆引きレシピについては、htmlspecialchars関数の第3引数を指定するように修正記事が出ています。立派な態度ですね。
一方、以下の書籍は、htmlspecialchars関数の第3引数を指定するように説明しています。

やはり、最近の書籍はhtmlspecialchars関数の第3引数を指定するように説明する傾向が伺えます。古い書籍でPHPを勉強された方や、古くからhtmlspecialchars関数の第3引数を指定しない方法で通している方は注意が必要です。

文字エンコーディングとして空文字列が指定されている場合の挙動の変化

次に、htmlspecialchars関数の第3引数に空文字列を指定している場合の挙動の変化について報告します。htmlspecialcharsはmbstring関数に比べて、指定できる文字エンコーディングが限られます。たとえば、Shift_JIS系の文字エンコーディングはShift_JISという指定のみが許され、SJIS-winやcp932は受け付けられません(参考:htmlspecialcharsのリファレンス)。しかし、PHP5.3までのhtmlspecialchars関数は、第3引数に空文字列が指定されていた場合、mbstring.internal_encodingにSJIS-winと指定していても、htmlspecialchars関数側ではShift_JISという指定に読み替えられていました。
一方、PHP5.4では、この読み替えが行われず、第3引数に空文字列を指定、かつmbstring.internal_encodingにSJIS-winと指定した場合、以下のエラーとなります。
PHP Warning:  htmlspecialchars(): charset `SJIS-win' not supported, assuming utf-8 in /home/wasbook/test/test2.php on line 12
htmlspecialcharsの第3引数に空文字列をしている例は見たことがないので、現実には問題にならないかもしれませんが、これはPHP5.4.0beta2のバグではないでしょうか。PHP5.3に比べて厳格にする理由が見あたらないからです。PHP5.4.0beta2のバグとして報告してもよいかもしれません。

まとめ

PHP5.4のhtmlspecialchars関数の挙動の変化について報告しました。既存のアプリケーションが動かなくなる可能性があり、PHP5.4移行にあたり注意が必要です。


追記
廣川さんの日記にて、htmlspecialcharsの第3引数に空文字列を指定した場合の挙動について報告がありました。PSとして、『SJIS-win、CP932、eucJP-winについてPHP 5.3と動作が同じとなるようにパッチをコミットしておきました。』ということですので、PHP5.4.0の正式版までには修正されると思われます。やはり、これはバグという認識で良かったようですね。廣川さんありがとうございました。


[PR]
安全なWebアプリケーションの作り方」電子書籍版販売しています。電子版はこちら

2011年10月13日木曜日

CookieのDomain属性は *指定しない* が一番安全

たまに誤解があるようですが、Cookieを設定する場合のDomain属性は *設定しない* のがもっとも安全です。以下、例示により説明します。

# このエントリは、はてなダイアリーの過去のエントリからの転載です

example.com上で稼働しているWebアプリケーションで以下の2種類のSet-Cookieについて比較します。
Set-Cookie: SESS1=F3B435;
Set-Cookie: SESS2=2A9E86; Domain=example.com;
SESS2の指定の方がドメインを明示しているので安全と思っている人がいるようですが、これは誤りです。Set-Cookieの際のDomain属性は、指定しない方が安全です。その理由は以下の通りです。
  • Domain属性を指定しないCookieは、Cookieを発行したホストのみに送信される
  • Domain属性を指定したCookieは、指定のホストおよびそのサブドメインのホストに送信される
すなわち、Domain=example.comを指定したCookieは、www.example.comにも送信されます。Domain属性を指定しないCookieは、example.comに送信され、www.example.comには送信されません。
これは、CookieのRFC2965(旧規格)、RFC6265(現規格)には明確に記述されています。
3.3.1 【中略】
Domain Defaults to the effective request-host. (Note that because there is no dot at the beginning of effective request-host, the default Domain can only domain-match itself.)
http://www.ietf.org/rfc/rfc2965.txt
4.1.2.3. The Domain Attribute
【中略】If the server omits the Domain attribute, the user agent will return the cookie only to the origin server.
http://www.ietf.org/rfc/rfc6265.txt
実際のブラウザで調査したところ、Domain属性のないCookieの挙動は以下の結果となりました。
IE9サブドメインにも送信される
Firefox 7.0.1RFC6265通り
Google Chrome 14.0.835.202RFC6265通り
Safari 5.1RFC6265通り
Opera 11.51RFC6265通り
iモード(P-07A)サブドメインにも送信される
Android 2.3.3RFC6265通り
IEiモードRFCに準拠していませんが、その他のメジャーなブラウザRFC通りの動作です。重大な問題とは言えませんが、RFC通りにしてもらいたいですね。
ところで、冒頭で「たまに誤解がある」と書きましたので、ドヤ顔で「CookieはDomain属性を明示すべし」と解説した文書がないか検索してみましたら、ありましたね。
Cookieを発行する際に与える属性には次のような注意を払う。
・domain={Cookie返送先ドメイン}
 domain属性に複数のサーバが含まれるようなドメインを指定しない。
 ドメインを階層深く指定することでCookieを使用するサーバが限定されて他のサーバへの露出を避けることができる。
 例
  × Set-Cookie: name=value; domain=example.jp; ...
  ◎ Set-Cookie: name=value; domain=foods.onlineshop.example.jp; ...
http://www.ipa.go.jp/security/awareness/vendor/programmingv2/contents/302.html
「階層深く」というあたりが妙な表現ですが、いずれの場合でもDomain属性を指定しない状態が、Cookieの送信先を限定できるという意味で一番安全です。
ところで、この文書、色々妙なことが書いてあります。続きを引用します。
・path={サイト内のパスのプレフィクス}
 path属性に示されたパスのプレフィクスが同一サーバに同居している別のアプリケーションリソースをカバーすることがないようにする。
 加えて、末尾には / を付けるようにする。
 例
  × Set-Cookie: name=value; domain=...; path=/
  △ Set-Cookie: name=value; domain=...; path=/application5/section3
  ◎ Set-Cookie: name=value; domain=...; path=/application5/section3/
http://www.ipa.go.jp/security/awareness/vendor/programmingv2/contents/302.html
引用した文書は、セッションハイジャック防止という文脈で書かれているのですが、その場合、CookieのPath属性を指定しても意味がないことが知られています。詳しくは、高木浩光@自宅の日記 - 共用SSLサーバの危険性が理解されていないをご覧ください。
また、(Pathの)「末尾には / を付けるようにする」というのも変です。CookieRFCにはこんな説明はありませんし、RFCで例示されているPath属性の例にも、末尾の / はありません。おそらく、Path=/fooと指定したCookieが /foobar というディレクトリにも送信されることを懸念しての指示だと思いますが、無用な懸念です。
まとめると、Domain属性とPath属性は以下の基準で判断します。
[注]
本エントリの記述内容は、すべて筆者の個人的見解であって、筆者の所属組織とは無関係です。

2011年10月12日水曜日

属性型JPドメインと地域型JPドメインに対するCookie Monster Bug調査

属性型JPドメインと地域型JPドメインに対するCookie Monster Bugについて調査した結果、複数のブラウザにおいて、これらドメイン名に対するCookie Monster Bugがあることがわかりました。
Cookie Monster Bugによる典型的な影響は、セッションIDの固定化攻撃を受けやすくなることです。
対策として、全てのサイトにおいて、Webアプリケーション側でセッションIDの固定化対策(後述)を実施することを推奨します。

調査結果

第1分類 第2分類 属性型JP
ドメイン
地域型JPドメイン
第2レベル 第3レベル






ケータイWeb iモード
Ezweb × ×
Softbank(1) × ×
Softbank(2)
PCサイト

ビューア
docomo
au × ×
Softbank(1) × ×
Softbank(2)
Android 標準 × ×
Opera × × ×
Firefox
iOS 4.3.5
PC IE 6~9 × ×
Firefox ~2.0.0.20 × × ×
3~
Google Chrome 14.0.835.202
Safari ~3.2.3 × × ×
4.0~
Opera 5 × ×
6~11.0 ×
11.1~11.51 × × ×

※凡例
  • 地域型JPドメインの第2レベルとは、tokyo.jpなど第2レベルのドメイン名でCookieが発行できるもの
  • 地域型JPドメインの第3レベルとは、chiyoda.tokyo.jpなど第3レベルのドメイン名でCookieが発行できるもの
  • 背景が赤のセルは現バージョンでバグのあるもの。無色は旧バージョンの参考情報
  • 携帯電話に関しては機種毎に仕様が異なる可能性が高く、全機種について調査したわけではないので、上記はあくまで抜き取りでの結果であることに注意されたし

※確認機種、バージョン等
  • iモード:P-07A(iモードブラウザ2.0)で確認
  • EZweb:W52T、biblioで確認(結果は同じ)。詳細は後述
  • Softbank(1): 821N, 932SHで確認。これらは非公式JavaScript対応機
  • Softbank(2): 944SH(公式JavaScript対応機)で確認
  • Android:Xperia Arc(Android2.3.3)にて確認
  • iOS:iPadで確認
  • Google Chromeは最新版(14.0.835.202)でのみ確認

調査方法

携帯電話(俗に言うガラケー)は試験環境でのテストができないため、インターネット上に環境を作り、調査しました。具体的には、筆者およびHASHコンサルティング(株)の所有する地域型JPドメインtokumaru.bunkyo.tokyo.jpおよび属性型JPドメインhash-c.co.jp上で発行するCookieを用いて調査しました。

Cookie Monster Bugが再現することを調査するためには、上記とは別のドメイン名で動作するサイトがあると確実です。このため、下記サイトの協力を得て調査を行いました。
  • tricorder.co.jp : 株式会社トライコーダ上野宣氏にご協力いただいた
  • otsuka.bunkyo.tokyo.jp : 左記サイト管理人の上田氏にご協力いただいた
上野氏と上田氏のご協力に深く感謝申し上げます。

EZwbbについての補足

現状のEZwebでは、HTTP通信の場合、ゲートウェイ(EZサーバー)上にてCookieが保管されます。今年の秋冬モデル以降の機種では、Cookieは常に端末に保存されるようになるようです(下記参照)。


このゲートウェイのdomain属性の挙動を調査した結果、独特の仕様であることが分かりました。以下、例示により説明します。

Webサイト http://www.example.co.jp/ において、以下をdomain属性としてCookieを発行する場合。

domain=co.jp
domain=.co.jp
domain=example.co.jp
domain=.example.co.jp
domain=www.example.co.jp

この際、有効となるCookieは、domain属性が .example.co.jp と www.example.co.jpのもののみです。また、このCookieは、example.co.jpドメインのサブドメインのうち、4レベルのサブドメイン(Cookieを発行したホストと同じレベルのドメイン)のみが受け取れます。すなわち、受け取れるホストを例示すると以下の通りです。

Cookieを受け取れるホスト
  www.example.co.jp
  home.example.co.jp  (domain=.example.co.jp のCookieのみ)
  a.example.co.jp  (domain=.example.co.jp のCookieのみ)

Cookieを受け取れないホスト
  example.co.jp
  hohe.www.example.co.jp

ここまでであれば、EZwebのCookieの仕様が独自というだけで、Cookie Monster Bugではありません。しかし、以下の例はCookie Monster Bugです。

例1: example.co.jpで発行された domain=.co.jp のCookieは、hash-c.co.jpにも tricorder.co.jp にも送信される。www.hash-c.co.jpやwww.tricorder.co.jpには送信されない。

例2:tokumaru.bunkyo.tokyo.jpで発行されたdomain=.bunkyo.tokyo.jpのCookieは city.bunkyo.tokyo.jpやotsuka.bunkyo.tokyo.jpには送信される。www.city.bunkyo.tokyo.jpやwww.otsuka.bunkyo.tokyo.jpには送信されない。

結果として、EZwebのゲートウェイのCookie処理にはCookie Monster Bugがありますが、ドメイン名そのものをホスト名とするサイトのみが影響を受けます。サブドメイン(www.example.co.jpなど)をホスト名として運用していれば、影響は受けません。

また、第1レベルのドメイン(TLD)について、domain指定することはできません。例えば、domain=.jpと指定したCookieは無効となります。

Operaについての補足

Operaは独自の方法でCookie Monster Bugに対応していると言われていました(参考:金床著「ウェブアプリケーションセキュリティ」)が、調査してみると、全てのバージョン(バージョン5以降)でCookie Monster Bugがあることがわかりました。不思議なことに、Operaバージョン6~11.0では、ごく限定的なパターン(3レベルの地域型JPドメイン)のみCookie Monster Bugがあったのに、バージョン11.10~11.51(本稿執筆時点の最新版)では、属性型JPドメインと、2レベルの地域型JPドメインのCookie Monster Bugが追加されてしまっています。

しかも、Opera11以降で導入されたアドレスバー上のドメイン強調表示を見ると、属性型JPドメイン、地域型JPドメインとも正しいドメイン名を強調表示しています。それにもかかわらず、Cookie Monster Bugがある原因は不明です。

現在、属性型JPドメインでCookie Monster Bugがあるブラウザは、EZwebのゲートウェイをのぞくと、全てOperaです。auのPCサイトビューアもOperaでした。

影響

Cookie Monster Bugによる典型的な影響はセッションIDの固定化攻撃(Session Fixation Attack, CWE-384)の影響を受けやすくなることです。WebアプリケーションにセッションID固定化の脆弱性がある場合、「なりすまし」の結果、以下の影響があります。
  • 利用者の重要情報(個人情報、メールなど)の閲覧
  • 利用者の持つ権限での操作(送金、物品購入など)
  • 利用者のID によるメール、ブログなどへの投稿、設定の変更など

サイト側対策

全てのサイト(Cookie Monster Bugの有無に関わらず)に対して以下を推奨します。
  • セッションIDとトークン以外をCookieとして保存しない
  • ログイン成功後にセッションIDを変更する、あるいはトークンによる対策を行う
  • セッションID以外をCookieに保持している場合、外部サイトから値をセットされた場合の影響を分析し、必要な対処を行う*1

*1:「体系的に学ぶ 安全なWebアプリケーションの作り方」では、セッションID以外にCookieに保存する例として、セッションID固定化対策、Cookieのセキュア属性不備対策に用いるトークンがありますが、これらのトークン(Cookie)に関してはCookie Monster Bugのセキュリティ上の影響はありません。

詳しくは以下を参照ください。

想定問答集


Q1:Cookie Monster Bugはブラウザの脆弱性なのか
A1:判断の分かれるところですが筆者はブラウザの脆弱性であると考えます

ブラウザ等インターネット対応のソフトウェアの処理内容は、RFCの規定に従うことになっています。Cookieの規定は、元々はNetscape社の仕様(archive.orgで参照有限会社futomiによる参考訳)があり、その後RFC2109(1997年)、RFC2965(2000年)が発行されましたが、実際にはこれらのRFCは普及せず、Netscape社のオリジナル仕様が広く使われていました。このため、現在広く使われているCookieの利用形態を文書化するような形で、RFC6265が今年の4月に発行されました。このRFC6265には、Cookie Monster Bugに関連する記述として、4.1.2.3. The Domain Attribute と 5.3. Storage Model があります。このうち、4.1.2.3の末尾を以下に引用します。
NOTE: For security reasons, many user agents are configured to reject Domain attributes that correspond to "public suffixes".  For example,  some user agents will reject Domain attributes of "com" or "co.uk".
参考訳:
セキュリティ上の理由から、多くのユーザエージェント(注:ブラウザのこと)は、public suffixと一致するDomain属性を拒否するように構成されます。 たとえば、いくつかのユーザエージェントは、「com」または「co.uk」のDomain属性を拒否します。
public suffix(effective top level domain; eTLD)については、高木浩光氏のエントリ「JPRSに対する都道府県型JPドメイン名新設に係る公開質問」の中に説明があります。上記の記述にあるように、Cookie Monster Bugへの対処はRFC6265上のmustやshouldの規定ではなく、参考情報のような扱いになっています。したがって、RFC6265を根拠として、Cookie Monster Bugが脆弱性であると判定することはできません。

しかしながら、Cookie Monster BugによるセッションID固定化の主要因は、Cookie Monster Bugそのものにあると考えられ、ブラウザ側で対処しないので、やむを得ずWebアプリケーション側で対処している状況と筆者は理解しています。

すなわち、Cookie Monser Bugはブラウザの脆弱性ではあるが、歴史的には、Netscape/Firefox、Safriに当該問題があり、現在でもIEでは完全に対処していないことは広く知られており、やむを得ずWebアプリケーション側で対応している、というのが筆者の理解です。



Q2:セッションIDの固定化以外の脅威はないの?
A2:セッションIDの固定化以外にも若干の問題があります

最近のブラウザには、URL中のドメイン名を強調表示する機能があります。これはフィッシング詐欺に対する保護機能として提供されるものです。IE9の該当機能の説明を引用します(ドメインの強調表示 - Microsoft Windowsより)。

詐欺的な Web サイトを回避する方法の 1 つに、アクセスしようとしている Web サイトのアドレスを正しく理解することが挙げられます。 Internet Explorer 9 にはドメインの強調表示機能があり、アドレス バーでドメイン名を強調表示することで正しい URL かどうかがひとめでわかります。 これにより、偽の URL でユーザーをだまそうとする詐欺的なサイトを注意でき、個人情報が漏えいする可能性を減らすことができます。

しかし、地域型JPドメインの場合、この機能は誤動作します。以下は大田区のホームページと筆者の所有する地域型JPドメインのIE9でのアドレスバーの表示です。

大田区ホームページのアドレスバー表示


tokumaru.bunkyo.tokyo.jpのアドレスバー表示






どちらもtokyo.jpが強調表示されていますが、これは誤りで、4レベルを強調表示するべきです。以下にOpera11.51での表示例を示します。


大田区ホームページのアドレスバー表示




tokumaru.bunkyo.tokyo.jpのアドレスバー表示





前述のように、Opera11.51には属性型JPドメインと地域型JPドメインのCookie Monster Bugがありますが、アドレスバーの強調表示は正しく行われています。なぜそうなっているかは不明です。

IEのケースに戻ると、ドメイン名の強調表示が誤っているため、フィッシング詐欺対策としてはかえって逆効果になると考えられます。意図的に、city.xxxx.xxxx.jpと紛らわしいドメイン名(例えば、cify.xxxx.xxxx.jp)を取得することで、地方公共団体のホームページと紛らわしいサイトを運営することができます。
さらに、今後JPRSが提供を予定している都道府県型JPドメインであれば、実在しない市名により、city.xxxx.xxxx.jp(xxxx.xxxxは市名と紛らわしいドメイン名)の運用が可能になると考えられます。アドレスバーのドメイン名強調表示は、このような詐欺行為に対して有効なはずですが、以下の理由により、有効に機能しないと予想されます。
  • 長年地域型JPドメインに対応していないIEが、さらに複雑なルールの都道府県型JPドメインに直ちに対応するとは考えにくい
  • 対応ブラウザであっても、ドメイン名のルールがややこしすぎて一般市民には理解困難
また、通常あり得ない想定ですが、理論的にはJavaScriptの同一生成元ポリシー(Same Origin Policy)に対する影響も考えられます。同一生成元ポリシーはデフォルトではホスト名の一致を要求しますが、document.domainの設定により、ドメイン(eTLD)の範囲内で制限を緩めることができます。このため、攻撃対象サイト内でdocument.domain="tokyo.jp"; が実行されている場合、このページをiframeの内部に表示させ、iframek外側から内容を読み取ることができます。ただし、サイト運営者がわざわざそのように設定する動機がないことから、この問題の影響はないか、極めて限定的であると考えられます。

同一生成元ポリシーの解説は、「体系的に学ぶ 安全なWebアプリケーションの作り方」を参照ください。


Q3:属性型JPドメインは使わない方がよいのか
A3:どのタイプのドメイン名を利用するかは、Cookie Monster Bugの有無だけではなく総合的に判断する必要があり、下記理由から属性型JPドメインの利用を推奨します

産総研が公表している「産総研 RCIS: 安全なWebサイト利用の鉄則」には、利用者をフィッシングの手口から守るためのドメイン名に対する要件として、以下を挙げています。
  • サービス提供者が保有するドメイン名を使う
  • まぎらわしくないドメイン名を使う
  • ドメイン名を利用者に周知する
属性型JPドメインは、上記を満たしやすい特性を持ちます(参考:JPドメイン名の種類 - 種類と対象 - / JPRS)。
従って、属性型JPドメインに対するCookie Monster Bugが一部のブラウザに存在するというだけの理由から属性型JPドメインを避けることはバランスに欠いた判断であり、属性型JPドメインを選択した上で、セッションID固定化対策をするのがよいと考えます。


Q4:地域型JPドメインは使わない方がよいのか
A4:Webサイトを運用するドメイン名としては、特別な理由がない限り地域型JPドメインは使わない方がよいと考えます

Cookie Monster Bugの対処はWebアプリケーション側で可能とはいえ、セッションIDの固定化脆弱性はまだあまり知られていない脆弱性であり、未対処のサイトが多いと予想されます。そのような状況下で、地域型JPドメインを積極的に利用する理由は見あたりません。属性型JPドメインのように、組織を識別しやすい特性があるわけでもないし、Cookie Monster Bugのあるブラウザの種類も増えてしまいます。また、前述のように、IEではドメイン名の強調表示が誤動作します。

このような状況下では、地域型JPドメインでのWebサイトの運用は避けた方が賢明であると考えます。


Q5:都道府県型JPドメインについての所見は?
A5:地域型JPドメインと同じ理由から、Webサイトのドメイン名としては避けた方がよいと考えます

現状のブラウザの実装状況から判断して、都道府県型JPドメインの登場は、現在の状況をさらに混乱させると予想します。そのような状況下で、Webサイトを運営するドメイン名として都道府県型JPドメインを選択することは当面避けるべきと考えます。


Q6:Cookie Monster Bugはドメインの問題ではなくて、Cookie仕様やブラウザの実装の問題ではないのか?
A6:その通りです

Cookie Monster Bugは前述のようにブラウザのバグであり、Cookieの仕様(RFC6265)に起因するものです。長期的には、これらが改善されるべきであると筆者は考えます。

しかしながらRFC6265は、RFC2965の発行から10年以上経ってようやく発行されたこと、長い間IEの地域型JPドメインのCookie Monster Bugが改善されていない実績などから、今後すぐにCookieの仕様やブラウザのCookie実装が改善される見通しに関しては悲観的に考えざるを得ません。

筆者はWebアプリケーションのセキュリティを専門とするコンサルタントであり、現在のWebの仕様や関連ソフトウェア(Webサーバー、開発言語、ブラウザ等)の状況を受け入れた上で、Webアプリケーションを安全にする方法を示すことが使命と考えます。この文書は、そのような立場で書いています。

地域型JPドメインや(将来の)都道府県型JPドメインを避けた方が良いというのは、あくまでも現状のブラウザにおけるCookieの実装を是認するという想定での話です。例えば、メールサーバーのドメイン名としてこれらドメイン名を用いる場合や、WebやCookieにとらわれない新しいテクノロジーを用いる場合は、上記は該当しません。


Q7:ケータイWebではCookieを使わない方が良いのか
A7:Cookie Monster Bugの存在に関わらず、今後はケータイサイトもCookieによるセッション管理に移行すべきと考えます。

Cookie以外のセッションIDの埋め込み場所としては、URLやケータイIDがありますが、いずれもセキュリティ上の課題があり、その解決は容易ではありません。これに対して、Cookie Monster Bugの対応は比較的容易です。

また、従来Cookieの使えないiモードブラウザ1.0の存在がCookie対応を難しくしていましたが、最近になって状況が変わってきました。最近のiモードブラウザ1.0のシェアを「モバイルサイトの3キャリア共通CSSと最新コーディング事情 - livedoor ディレクターブログ」から、調べてみます。
このエントリによると、ケータイWebのアクセスシェアでiモードは46.4%、そのうちiモードブラウザ1.0のシェアは30.9%ということです。これにより、ケータイWeb中のiモードブラウザ1.0のアクセスシェァは、約14%となります。ケータイの世界でも、そろそろCookie対応機種にのみ対応することを検討してもよい状況になってきたと言えます。

14%を切り捨てるのが忍びない場合でも、Cookieの使えない場合のみURLにセッションIDを埋め込むこともできます。詳しくは以下を参考にしてください。


Q8:Cookie Monster BugによりCookieの盗聴はできるの?
A8:できません


Q9:余計なCookieの削除はしなくてよいの?
A9:余計なCookieの削除は必須ではありません

Cookie Monster Bugを用いた攻撃の結果、攻撃者から注入されたCookieとアプリケーションが発行するCookieが同じ名前で複数登録された状態になります。これに対して、余計なCookieを削除するべきかどうかという質問です。

セッションIDの固定化攻撃への対策としては、余計なCookieを削除は必須ではありません。しかし、同名Cookieがブラウザから送られることにより、アプリケーションが誤動作する可能性はあります。詳しくは以下の参考サイトをご覧ください。

この対処としては、余計なCookieの削除の他に、複数のCookie(セッションIDなど)を検知して、もし複数の同名Cookieがあれば、エラーとして処理を停止する実装も考えられらます。すなわち、同名Cookieについては以下の3種類の実装が可能です。
  • 何もしない(セッションIDの振り直は行う)
  • 認証成功時に余計なCookieを削除する
  • 同名Cookieを検知してエラーとする
同名Cookieを削除してしまうと、セッションIDの固定化攻撃が行われていても、攻撃を見逃して正常処理を継続してしまうことになります。これに対して、同名Cookieを検知してエラーにすると、攻撃(の可能性)を検知できます。

これら実装には一長一短があるため、サイト運営者の判断により仕様を決定することになります。


Q10:私の管理するサイトにセッションIDの固定化脆弱性があるかどうすれば分かりますか?
A10:ログイン前後でセッションIDが変化するかどうかで判定できます

この問題を含めてWebアプリケーションの簡易的な脆弱性検査の目的に「ウェブ健康診断仕様」が利用できます。仕様書は財団法人地方自治情報センターのサイトからダウンロードできます。セッションIDの固定化は、同仕様書K-1に記述されています。


Q11:色々ややこしくてよく分からないんだけど、私のサイトが問題ないか調べてもらえませんか?
A11:未承諾広告※個別サイトの問題についてはHASHコンサルティング株式会社のサービスがご利用いただけます

HASHコンサルティング株式会社では、上記のような調査結果をブログや寄稿記事、勉強会における講演等の形で広く公開しております。これらの情報は無料でご利用頂けます。
一方、サイト個別の特性に応じた調査が必要な場合や、セキュリティ担当者がいないなど、公開した情報だけでは個別の問題に対して判断ができない場合は、HASHコンサルティング株式会社の有償サービスがご利用頂けます。問い合わせはこちらの問い合わせフォームからお願い致します。

[PR]

「体系的に学ぶ 安全なWebアプリケーションの作り方」のDRMフリーPDFによる電子版が販売開始しました。今なら、キャンペーン価格¥1,800-(36%OFF)でお求め頂けます(10月17日まで)。購入はこちらから。

2011年9月27日火曜日

都道府県型JPドメインがCookieに及ぼす影響の調査


JPRSからのプレスリリース『JPRSが、地域に根ざした新たなドメイン名空間「都道府県型JPドメイン名」の新設を決定』や報道などで「都道府県型JPドメイン」というものが新設されることを知りました。
都道府県型JPドメインとは、現在活発に使われていない地域型ドメインを活性化する目的で、地域型ドメインの制約(ドメイン名が長い、一人・一団体あたり1つまで)を簡略化しようというもののようです。
しかし、現在の地域型ドメインは、ブラウザにとって処理がややこしいもので、IEなどは昔からまともに対応していません。このため、Cookie Monster Bugという脆弱性になっているという経緯があります。このルールをさらに複雑にすることになるということから、ブラウザセキュリティに関心の高い人たちが騒ぎ始めています。

そこで、高木浩光氏の日記「JPRSに対する都道府県型JPドメイン名新設に係る公開質問」の以下の部分に関して、調査をしてみました。
何もしなければ、「都道府県型JPドメイン名」の登録が始まっても、cookieを利用できないなどの欠陥ドメイン名となることが予想される。
調査は、「何もしない」状態、すなわち現在のブラウザで、都道府県型ドメインをシミュレートして、Cookieの受け入れられる状態を確認しようというものです。

まず、ドメインとしては、地域型ドメイン toku.tsu.mie.jp と、都道府県型JPドメイン toku.mie.jp の2種類を実験環境に用意しました。

現在の地域型ドメインは、原則として4レベルのドメインになります。但し、都道府県のドメイン(pref.mie.jp等)は例外です。これに対して、都道府県型JPドメインは3レベルのドメインになることがウリです。都道府県型ドメインの「ややこしさ」とは、この4レベルと3レベルのドメインが混在することで、その切り分けには、市町村の名前全て(tsu.mie.jpなど)をブラウザ側に保持する必要が生じることです。

ホスト名(FQDN)としては、ドメインそのものと、www.で始まるもののの2種類を用意しました。すなわち、ホスト名は以下の4種類です。

地域型ドメイン(toku.tsu.mie.jp)に属するもの
  • toku.tsu.mie.jp
  • www.toku.tsu.mie.jp
都道府県型JPドメイン(toku.mie.jp)に属するもの
  • toku.mie.jp
  • www.toku.mie.jp
これらホストに対して、以下の5種類のdomain属性を持つCookieをセットしてみて、どのCookieが受け入れられるかを調べました。
  • mie.jp
  • tsu.mie.jp
  • toku.mie.jp
  • www.toku.mie.jp
  • toku.tsu.mie.jp
  • www.toku.tsu.mie.jp
まず、Firefox 6.0.1での結果を下表に示します。

Firefox 6.0.1
Cookie 地域型ドメイン 都道府県型JPドメイン
toku.tsu.mie.jp www.toku.tsu.mie.jp toku.mie.jp www.toku.mie.jp
mie.jp
tsu.mie.jp
toku.mie.jp ×
www.toku.mie.jp
toku.tsu.mie.jp
www.toku.tsu.mie.jp

ホスト www.toku.mie.jpに対して、domain=toku.mie.jpのCookieが受け入れられていません。これは、現在の地域型ドメインのルール(4レベルのドメイン)に則したものです。
一方、ホストtoku.mie.jpに対して、domain=toku.mie.jpのCookieが受け入れられているのは規約上微妙な気がします。そのような地域型ドメインのホストは現在の仕様上はないはずですが、現にホストが存在していることを優先して、Cookieを受け入れているのでしょうか。

次に、Chrome 14.0.835.168 および 15.0.874.24 beta-m での結果を示します。どちらも同じ結果です。

Chrome 14.0.835.168 15.0.874.24 beta-m
Cookie 地域型ドメイン 都道府県型JPドメイン
toku.tsu.mie.jp www.toku.tsu.mie.jp toku.mie.jp www.toku.mie.jp
mie.jp
tsu.mie.jp
toku.mie.jp × ×
www.toku.mie.jp
toku.tsu.mie.jp
www.toku.tsu.mie.jp

Firefoxの場合と1箇所異なっています。domain=toku.mie.jpのCookieはいかなる場合にも受け入れられていません。これは現在の地域型ドメインの仕様を重視した結果と思われます。
その結果、高木浩光氏の言われる「cookieを利用できないなどの欠陥ドメイン名と」なっています。現状のChromeの仕様が仮にこのままだとすると、都道府県型JPドメインであっても3レベルのままでは使用できず、www.などのサブドメインをつけなければ使用できないことになります。

続いて、IE9での結果です。

IE9 (9.08112.16421)
Cookie 地域型ドメイン 都道府県型JPドメイン
toku.tsu.mie.jp www.toku.tsu.mie.jp toku.mie.jp www.toku.mie.jp
mie.jp ○(cm) ○(cm) ○(cm) ○(cm)
tsu.mie.jp ○(cm) ○(cm)
toku.mie.jp
www.toku.mie.jp
toku.tsu.mie.jp
www.toku.tsu.mie.jp

よく知られているように、IEは地域型ドメインに対してCookie Monster Bugがあります(cmをつけた箇所)。Cookie Monster Bugとは、本来受け入れてはならない上位のドメインについてCookieを発行できるバグのことです。
現在の地域型ドメイン、例えばtoku.tsu.mie.jpでサイトを運営していると、mie.jpドメインでのCookieが発行できるわけですから、これはpref.mie.jp(三重県のドメイン)やcity.tsu.mie.jp(津市のドメイン)にも適用されるCookieが発行できることになります。これにより、セッションIDの固定化攻撃がやすやすくなるなどの影響かあります。
都道府県型JPドメインの運用が始まった後のIEの対応はわかりませんが、長年地域型ドメインのCookie Monster Bugを放置しているという実績から考えると、都道府県型JPドメインにすぐさま対応するは思えません。

次に、Safariでの結果ですが、これはFirefoxと同じパターンです。

Safari 5.0.2(7533.18.5)
Cookie 地域型ドメイン 都道府県型JPドメイン
toku.tsu.mie.jp www.toku.tsu.mie.jp toku.mie.jp www.toku.mie.jp
mie.jp
tsu.mie.jp
toku.mie.jp ×
www.toku.mie.jp
toku.tsu.mie.jp
www.toku.tsu.mie.jp

最後にOperaでの結果です。

Opera 11.51
Cookie 地域型ドメイン 都道府県型JPドメイン
toku.tsu.mie.jp www.toku.tsu.mie.jp toku.mie.jp www.toku.mie.jp
mie.jp ○(cm) ○(cm) ○(cm) ○(cm)
tsu.mie.jp ○(cm) ○(cm)
toku.mie.jp
www.toku.mie.jp
toku.tsu.mie.jp
www.toku.tsu.mie.jp

この結果を見て驚きました。OperaにもCookie Monster Bugがあります。最初調査方法の問題かと思い、調査方法を変えて調べてみましたが、結果は変わりません。どうも最近のOperaには地域型ドメインに対するCookie Monster Bugがあるようです(*1)。
この結果はIEと同じですので、影響もIEと同じです。

まとめ
現在のブラウザの仕様が変わらないという前提で、都道府県型JPドメインの運用が始まった際の影響を調べました。
Firefox、Chrome、Safariについては、3レベルのドメインについてCookieが受け入れられない場合があります。この結果、Chromeの場合3レベルのドメインではCookieがまったく利用できません。FirefoxとSafariでは、ホスト名そのままのCookieは利用できますが、サブドメイン間でCookieを共有できないという問題につながります。
IEとOperaについては、Cookie Monster Bugのため、Cookieを安全に利用できないという問題があります。
この結果をどう評価するかですが、私は、高木浩光氏の日記で指摘された「cookieを利用できない」という欠陥よりも、IEとOperaのCookie Monster Bugの方が気になります。とくに、IEのシェアは現在でもかなり高いため、地域型ドメインおよびその後継である都道府県型JPドメインでは、Cookieを安心して使えず、セキュリティ上の大きな障害になると考えます。

*1: 地域型ドメインと都道府県型JPドメインのシミュレーションには、DNSキャッシュサーバーにて実験用のドメインを指定する方法を最終的に用いました。

2011年9月26日月曜日

PostgreSQLは標準でバックスラッシュをエスケープしない仕様になった

PostgreSQL9.1の仕様変更にて、デフォルト時の設定として、standard_conforming_stringsがonとみなされるようになりました。この仕様変更により、デフォルト設定でのPostgreSQLは、バックスラッシュをエスケープする必要がなくなり、ISO規格のSQLと同様のエスケープルール(シングルクォートを重ねるのみ)となります。

PostgreSQLの文字列リテラルは、元々MySQL同様に、バックスラッシュをエスケープする仕様でした。その後、リリース8.1にて、設定パラメータ standard_conforming_strings が追加され、この値が on の場合、バックスラッシュをエスケープしない(ISO規格と同様の)仕様となりました。従来のリリースでは、standard_conforming_stringsを指定しない場合offとみなされていました。これは、後方互換性維持のためでしょう。

リリース8.1のドキュメントには以下のように記述されています。
standard_conforming_stringsの値は読み取りのみです。アプリケーションでは、この値を読み取ることで、どのようにバックスラッシュが解釈されるかが分かります。(また、このパラメータが存在するかどうかで、E''文字列構文がサポートされているかどうかが分かります。)今後のリリースでは、standard_conforming_stringsは真になる予定です。
ここで、E''文字列構文という用語が出てきました。これは、PostgreSQL固有の機能で、standard_conforming_stringsの設定に関わらずバックスラッシュによるC言語風のエスケープを行う文字列形式です。

ここで、standard_conforming_strings設定と、バックスラッシュのエスケープ要・不要の関係を下表にまとめました。

standard_conforming_strings設定とバックスラッシュのエスケープの関係
standard_conforming_stringsリリース9.0.4以前リリース9.1以降
offエスケープ要エスケープ要
指定無しエスケープ要エスケープ不要
onエスケープ不要エスケープ不要

表からも分かるように、PostgreSQLリリース9.1にバージョンアップして挙動が変わるのは、standard_conforming_stringsの設定をしていない場合です。この場合は、standard_conforming_stringsをoffにすることで従来通りの挙動となります。

しかし、standard_conforming_stringsの設定を変更しない場合は、既存のアプリケーションはどのような影響を受けるでしょうか。以下、PHPとPerlで記述されたアプリケーションについて、この変更の影響を検討してみます。

PHPのPostgreSQL関数(pg_xxxx)やPDOなどには、SQL文字列のエスケープ用の関数が用意されています。PostgreSQL関数ではpg_escape_string関数、PDOではquoteメソッドが該当します。これらを使用している場合、standard_conforming_stringsのオプションを自動的に反映して、適切にエスケープしてくれます。
DBI/DBDのPgやPgPPを使っている場合、quoteメソッドで文字列をエスケープしている場合も同様です。
これらの挙動を「a\'」という文字列がどうエスケープされるかによって調査しました。

standard_conforming_stringspg_escape_stringquote(PDO)  quote(Pg)quote(PgPP)
offa\\'''a\\'''E'a\\'''E'a\\\''
指定無しa\'''a\'''E'a\\'''E'a\\\''
ona\'''a\'''E'a\\'''E'a\\\''
※各モジュールのバージョン
PHP: 5.3.8
Pg: 2.18.1
PgPP: 0.08

quoteという名前のメソッド(PDO, PG, PgPP)は、いずれもエスケープするだけでなく、シングルクォートで文字列を囲むことにご注意下さい。
上表から、これらの関数・メソッドを使っていれば、文字列は正しくエスケープされることが分かります。pg_escape_stringとPDOのquoteメソッドは、通常の文字列リテラル形式を用い、バックスラッシュのエスケープをstandard_conforming_stringsに応じて切り替えています。
これに対して、PerlのPgとPgPPは、E''文字列形式を用いることによって、standard_conforming_stringsの影響を回避しています。また、プレースホルダを使ってSQLを呼び出している場合も、standard_conforming_stringsの影響は受けません。

一方、エスケープにaddslashesや自作のエスケープ関数を用いている場合は、対応が必要となります。また、PgPPの古いバージョンを使っている場合もstandard_conforming_stringsの設定を考慮しないので注意が必要です。

既存アプリケーションのリリース9.1対応では、standard_conforming_stringsの意味を変えない方が無難だと思います。このため、既存アプリケーションでPostgreSQLのバージョンを9.1以降に変更する場合は以下のようにすればよいでしょう。
  • standard_conforming_stringsを明示している場合はそのままでよい
  • standard_conforming_stringsを明示していない場合はoffを指定する
これにより、standard_conforming_stringsのデフォルト値の変更を吸収できます。

一方、新規アプリケーション場合は、standard_conforming_stringsを指定しない(あるいはonにする)と良いでしょう。その理由は、こちらの方がISOのSQL標準であり、またバックスラッシュをエスケープ対象にすると、セキュリティ上の問題になりやすいからです。MySQLとの互換性を気にする人もいるでしょうが、PDOやDBIなどの抽象度の高いライブラリを使って記述することにより、エスケープ対象の文字をアプリケーション側で意識する必要はなくなり、互換性も向上します。
さらに言えば、プレースホルダを用いてSQL呼び出しすることを強く推奨します。これにより、そもそも文字列リテラルのエスケープが必要なくなり、性能・互換性・セキュリティともに改善されます。

[PR]
安全なWebアプリケーションの作り方」電子書籍版9月28日(水)販売開始します。くわしくはこちら

2011年8月27日土曜日

Apache killerは危険~Apache killerを評価する上での注意~

Apacheの脆弱性(CVE-2011-3192)いわゆるApache killerが話題になっていますが、その脅威については一部誤解があるようです。

以下は、非常に脅威とする報告の例です。
一方今回のはプロセスの肥大化を伴うので、実メモリ消費して更にスワップも使い尽くしてOS毎激重になったあげくLinuxとかの場合はOOM Killer発動と、他のプロセスや場合によってはOSを巻き込んで逝ってしまいます。
CVE-2011-3192 Range header DoS vulnerability Apache HTTPD 1.3/2.xより引用
以下は、それほど脅威でなかったとする報告の例です。
pooh.gr.jp は結構頑丈だったので 60 並列でやっと CPU idle 30% まで減らせた。
Apache Killer (CVE-2011-3192) 対策 for CentOS 5.6より引用
この差はなんでしょうか。サーバーが「結構頑丈だった」せいでしょうか。

それだけではありません。Apache killerを評価する上で重要なパラメータが2つあります。以下に示します。
  • ダウンロード指定するコンテンツのバイト数
  • ApacheのMaxClients設定

なぜコンテンツのバイト数が重要か

既に報告されているように、Apache killerはRangeヘッダに多くのパラメータを指定することにより、Apacheの使用メモリを増大させます。現在出回っているPoCの生成するRangeヘッダを以下に示します。
Range: bytes=0-,5-0,5-1,5-2,5-3,5-4,5-5,5-6,5-7,5-8,5-9,5-10,5-11,5-12,5-13,5-14,5-15,
5-16,5-17,5-18,5-19,5-20,5-21,5-22,5-23,5-24,5-25,5-26,5-27,5-28,5-29,5-30,5-31,5-32,

--CUT--

5-1281,5-1282,5-1283,5-1284,5-1285,5-1286,5-1287,5-1288,5-1289,5-1290,5-1291,5-1292,
5-1293,5-1294,5-1295,5-1296,5-1297,5-1298,5-1299
ご覧のように、5-1,5-2,5-3,…,5-1299 までのRange指定が含まれます。このため、URLで指定するコンテンツのデータサイズが1299バイト以上ないと、このPoCの「真価」が発揮されないと予想されます。
これを実験で試してみました。killapache.plをN=1で走らせ、URLで指定されるコンテンツ(/index.html)を0バイトから2000バイトまで変化させて、apacheのプロセスのメモリサイズを測定しました(追記:MaxClientsは3としました。理由は後述)。まずは生データです。

以下は、グラフにしたものです。

予想通り、コンテンツサイズが1300バイトまではプロセスサイズが大きくなりますが、それ以上ではプロセスサイズは変わりません。すなわち、Apache killerを評価する上では、コンテンツのサイズを1300バイト以上にすることが重要なポイントとなります。

pooh.gr.jpの評価では、robots.txt(約30バイト)を指定していたため、Apache killerはまったく真価を発揮しておらず、単なるF5攻撃と変わらない状況だったと予想されます。


MaxClients設定が重要な理由

ここまで説明すると、Apache killerの評価をする上でMax Clientsが重要である理由を説明することも容易になります。Apache killerのリクエストを受け付けると、元々24メガバイトだったApacheプロセスの消費メモリが96メガバイトと4倍になります。しかし、MaxClientsが十分余裕を持って設定されていれば、プロセスメモリが増大しても処理を継続することができます。

徳丸の評価結果

私も最初上記をよく理解しておらず、「あれ、Apache killer大したことない?」と誤解しかかったり、「評価環境が非力なので差が出ないのかな」と思ったりしました。上記に気がついた後、以下の条件で評価してみました。

VMのメモリ割りあて: 2ギガバイト
swap:512メガバイト
コンテンツのサイズ:約50Kバイト(一般的なコンテンツのサイズを想定)
MaxClients: 50
OS:CentOS5.6(32ビット)


上記条件でApache killerを走らせた場合、必要なメモリサイズは96 * 50 = 4800メガバイトとなり、メモリ+swapがまったく不足する状況となります。
結果は、ping以外はまったく応答しなくなりまりした。sshからコマンドを打つことも、VMwareのコンソールからログインすることもできなくなり、リセットするしかない状態でした。

このエントリを読んだ皆様、Apache killerはやはり凶悪です。くれぐれも油断なきよう、至急の対策をお勧めします。
私は、Apache2.2を使っていましたので、アドバイザリに従い、httpd.confに以下を追加しました。

# Drop the Range header when more than 5 ranges.
# CVE-2011-3192
SetEnvIf Range (?:,.*?){5,5} bad-range=1
RequestHeader unset Range env=bad-range
# We always drop Request-Range; as this is a legacy
# dating back to MSIE3 and Netscape 2 and 3.
RequestHeader unset Request-Range

2011年8月24日水曜日

PHP5.3.7のcrypt関数のバグはこうして生まれた

昨日のブログエントリ「PHP5.3.7のcrypt関数に致命的な脆弱性(Bug #55439)」にて、crypt関数の重大な脆弱性について報告しました。脆弱性の出方が近年まれに見るほどのものだったので、twitterやブクマなどを見ても、「どうしてこうなった」という疑問を多数目にしました。
そこで、このエントリでは、この脆弱性がどのように混入したのかを追ってみたいと思います。

PHPのレポジトリのログや公開されているソースの状況から、PHP5.3.7RC4までこのバグはなく、PHP5.3.7RC5でこのバグが混入した模様です。RC5はPHP5.3.7最後のRelease Candidateですから、まさに正式リリースの直前でバグが入ったことになります。
バグの入る直前のソースは、ここの関数php_md5_crypt_rから参照することができます。以下に、おおまかな流れを図示します。まずはバグの入る前です。


上図の各処理の概要は以下の通りです。
  1. passwdはstatic配列なので初期値は全て0('\0')
  2. MD5を示すマジック「$1$」をpasswdの先頭にmemcpyでコピー
  3. ソルトを定位置にstrlcpyでコピー
  4. 現時点の文字列末尾に$をstrcatで追加
  5. ハッシュ値を定位置に書き込み
  6. 文字列の終端を示す'\0'を書き込み
このような状態で、ソースコードの静的解析が実施され、その結果、(4)のstrcatが指摘されたようです。strcatはバッファ長を指定できないので、データの内容によってはバッファオーバーフローの原因になります。それを指摘されたのでしょう。もっとも、この処理の場合はデータが固定長なので脆弱性の心配はないのですが、コミットログのコメントには以下のように書いてあります。
Make static analyzers happy
静的解析ツールの警告表示を消したかったのでしょうね。このため、以下の変更が行われました(差分)。

strcat(passwd, "$");
  ↓
strncat(passwd, "$", 1);

strncatは書き込む文字列の最大長を指定するstrcatの改良版です。この場合は1が指定されているので、最大1文字書き込み、その後に終端の'\0'を書き込みます。

この時点ではバグは入っていません。問題は、この後です。いったんstrncatに変更した箇所が、さらにstrlcatに変更されています。

strlcat関数はstrncatの改良版です。strncatは、追加する文字列の最大長を指定しますが、元の文字列長と追加文字列、さらに終端の'\0'のトータルの長さを意識しなければならないという点で使いにくいという問題があります。これに対して、strlcatはバッファ長を指定するので、文字列長の計算を呼び出し側で意識しなくてもよいという利点があります。バッファオーバーフロー対策としてはstrncatよりも確実です。
この変更点を以下に示します(差分)。

strncat(passwd, "$", 1);
  ↓
strlcat(passwd, "$", 1);

ここで不幸にもバグが入りました。strncatとstrlcatでは、第3パラメータの意味が異なります。strncatは、追加する文字列の最大長なので、1で問題ありません。一方、strlcatはバッファ長を指定するので、既に"$1$"とソルト(8文字)の都合11文字が入っているバッファに対して、バッファ長1を指定したことになり、「バッファは既にいっぱいなので文字を追加する余地はない」と解釈されます。その結果、"$"の書き込みは行われません。バッファはそのままの状態になります。
ここで、strlcatへの変更後の処理を以下に示します。(4)の部分が変更点です。


処理(5)と(6)でハッシュ値と文字列終端'\0'が書き込まれますが、その直前の箇所が'\0'のままです。このため、C言語の文字列としては、"$1$"とソルトだけでちぎれた状態になります。このため、肝心のハッシュが出力されないという結果になりました。

以上がBug #55439の混入した経緯です。

ところで、strlcpy/strlcatの仕様をWikipediaで確認していたところ、興味深い記述を見つけました。
一方で、GNU Cライブラリ (glibc) の開発者たちは、GNU Coding Standardsで禁じられている「長い行を黙って切り詰める」関数である、このような仕様の関数はバグである、いい加減なプログラムを助長してしまう、新たなセキュリティ問題を生む、など否定的な見解を示しており、標準規格に含まれない限りはglibcには実装しない意向である。
上記の意見には賛否両論あるところでしょうが、今回のケースでは、glibcの開発者達の予想が不幸にも的中してしまったことになります。

プロジェクトマネジメント上の問題としては、テスト不足というのは明らかですが、その前段階の問題として、RC5というリリース直前の状態で不急の修正をしたことが大きな要因だと考えます。

追記

今回の原因を作ったRasmus Lerdorf(PHP/FIのオリジナル開発者)がGoogle+で今回の経緯を説明しています。英語ですが、興味のある方はご覧下さい。私のエントリの修正は必要なさそうです。

追記2

今さらですが、PHP5.3.7のmake testを走らせたところ、ちゃんとテストでFAILしているのですね。
TEST 8080/8990 [ext/standard/tests/strings/crypt.phpt]FAIL crypt() function [ext/standard/tests/strings/crypt.phpt]
テスト(crypt.phpt)の内容を見ると非常に単純なテストであり単体レベルではもっと色々なパターンで試験すべきだと思います。しかし、せめてこのテストを単体テストで実施することと、テストしたことをコミット時に確認していれば、バグ流出は防げたと思います。