2012年4月9日月曜日

PHPのescapeshellcmdを巡る冒険

以前、ブログ記事「PHPのescapeshellcmdの危険性」にて、escapeshellcmd関数の「余計なお世話」によって危険性が生まれていることを指摘しましたが、その後大垣さんによって修正案が提示され、結局「それはマニュアルの間違い」ということで決着が着いたようです。ところが、この議論とは別のところで、escapeshellcmdはPHP5.4.0で挙動が少し変わっていることが分かりました。

経緯

  • 2011/1/1 徳丸が「PHPのescapeshellcmdの危険性」を書いて、クォート文字がペアになっている場合にエスケープしないという仕様が余計なお世話であり、危険性が生じていることを指摘
  • 2011/1/7 大垣さんがブログエントリ「phpのescapeshellcmdの余計なお世話を無くすパッチ」にて修正案を提示
  • 2011/10/23 廣川さんが、大垣さんのパッチ案を少し修正してbugs.php.netに提案。修正案は却下され、マニュアルを修正することに
  • 2012/3/1 PHP5.4.0リリース。実はescapeshellcmdの仕様が変わっていた

MLでの議論

大垣さんと廣川さんのパッチ提案に対して、lbarnaudさんが、「いやいや、escapeshellcmdはコマンド全体をエスケープするもので、提案のような使い方は想定してないよ、マニュアルの例も間違いだ」と指摘します。
歴史的に見て、この指摘は正しいように思いました。そうでないと、escapeshellcmdとescapeshellargという別の関数が存在する理由が説明できません。
すなわち、元々PHPには、コマンド全体をエスケープするescapeshellcmdが存在したが、それだとコマンドの引数を追加する攻撃が出来てしまうので、escapeshellargという関数が後から追加されたのでしょう。
ということで、escapeshellcmdは修正せず、マニュアルの方を修正するということで決着がつきました。詳細は、bugs.php.netの方を参照下さい。マニュアルは既に修正されています。
ところが・・・

PHP5.4.0でescapeshellcmdの仕様が変わっていた

このエントリは、上記の顛末を報告するつもりで書き始めたのですが、念のためPHPの様々なバージョンでescapeshellcmdの挙動を確認したところ、PHP5.4.0でこの関数の仕様が変わっていたことが分かりました。

サンプルスクリプトは以下です。
<?php
  echo phpversion(), "\n";
  $a = 'echo "foo    bar" baz"';
  echo $a, "\n";
  echo escapeshellcmd($a), "\n";
  system(escapeshellcmd($a));

PHP5.3.10での実行結果
5.3.10
echo "foo    bar" baz"
echo "foo    bar" baz\"
foo    bar baz"

PHP5.4.0での実行結果
5.4.0
echo "foo    bar" baz"
echo foo    bar baz\"
foo bar baz"

「' および " は、対になっていない場合にのみエスケープされます」ではなく、「対になっている' および " は削除されます」という動作に変わっています。その結果、echoの表示も、複数の空白が一つにまとめられています。マニュアルには、この仕様は記載されていないようですし、変更履歴にも載っていません。
この修正も余計なお世話としか思えませんが、私の結論は変わりません。
ということで、先のエントリの結論を再掲します。

まったく余計なお世話としか言いようがありません。この仕様では、恐ろしくてescapeshellcmdは使えませんし、マニュアルにここまではっきり書いてある仕様を今さら変えられないでしょう。escapeshellcmdはお蔵入りするしかないと思います。幸い、escapeshellargの方はまともな仕様と思われますので、escapeshellargで代替してください。
ただし、そもそもOSコマンドを呼ぶのがよいかとか、もっと良い方法はないのかという疑問が出てきます。もっと良い方法はあります。それは、本が出てからのお楽しみ、ということで。

追記

廣川さんに確認したところ、上記はPHP5.4.0のバグだそうで、PHP5.4.1で元の仕様に戻るだろうということです。PHP5.4.0でescapeshellcmdを使用する際はご注意ください。廣川さん、確認ありがとうございました。

[PR]
Webサイトのセキュリティ強化策についての相談は、HASHコンサルティング株式会社まで。
「安全なWebアプリケーションの作り方」DRMフリーのPDFによる電子版もあります。

2012年4月5日木曜日

PHPの組み込み関数で例外を発生させる方法

このエントリではPHPの組み込み関数でエラー時に例外を発生させる方法を紹介します。デフォルト状態では、PHPの組み込み関数の大半はエラー時に例外を発生させません。

前のエントリで、PHPのheader関数は戻り値を返さず、エラー時に例外も発生させないことを紹介しました。これは酷い仕様だと思うのですが、どうすればエラーハンドリングできるかを考えてみました。

header関数の場合、エラー(警告)そのものは出ているので、以下の二つの方法が候補として考えられます。
  • error_get_last関数で直近のエラーを取得してエラー処理する
  • set_error_handlerで定義したエラーハンドラ関数でエラー処理する
どちらもモダンな書き方とはほど遠い感じです。
前者は、BASICのon error resume nextを連想させますし、直近のエラーがどの箇所で起こったかは簡単には識別できないので、過去のエラーを捕捉してしまいそうです。行番号はとれますが、行番号で判断するのはよくないでしょう。これもBASICみたいですね。
一方、エラーハンドラでエラー処理するのもよくありません。エラーハンドラにはあらゆるエラーが飛んでくるので、ログを吐いてプログラムを終了させるくらいしか現実にはできないでしょう。

そういう問題意識で「パーフェクトPHP」を読んでおりましたら、組み込み関数から例外を発生させる方法がちゃんと書いてありました(同書P160)。
また、エラーハンドラを設定することにより、PHPの標準のエラーを例外に変換して投げることもできます。エラーを例外に変換するには、次のようにエラーハンドラを設定します。例外の種類には、定義済みのErrorExceptionを利用しています。
set_error_handler(function ($errno, $errstr, $errfile, $errline ) {
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});
PHPのパワープログラマには常識なのかもしれませんが、私は「なるほどねぇ~」と思いました。ブログ記事などでも見かけないようです。ただし、PHPのマニュアルには書いてありました。

これを使って、header関数の例外処理のサンプルを書いてみました。
<?php
// エラーを例外に変換
set_error_handler(function ($errno, $errstr, $errfile, $errline ) {
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});
// リダイレクト関数…ただし、エラーになる
function redirect() {
  header("Location: http://www.yahoo.co.jp\nSet-Cookie: aaa=bbb");
}
try {
  redirect();
} catch (ErrorException $e) {
  echo 'Redirect error: ' . htmlspecialchars($e->getMessage(), ENT_COMPAT, 'UTF-8');
  // 必要な後始末
}
結果は、「Redirect error: Header may not contain more than a single header, new line detected.」と表示されます。
ということで、エラーを例外に変換する方法を紹介しました。パーフェクトPHPは本当に良い本ですね。
ただし、これをやると、警告を含む全てのエラーで例外が発生するので、ちまたでよく見るような、エラー処理もろくにしていないヌルい書き方はできなくなり、プログラム全体できちんと例外の設計をする必要があります。本来そうあるべきですけどね。

追記

はてなブックマークコメントで「$errno == E_WARNING のときは例外投げないようにすれば」というコメントを頂戴しましたが、そうもいかないように思います。
そもそもの発端だったheader関数のエラーはE_WARNINGです。しかし、ヘッダを送信しようとしてできなかったという事象は「WARNING」という語感とは裏腹な、かなり深刻なものだと思います。また、E_WARNINGより重いエラーであるE_ERRORは「重大な実行時エラー。これは、メモリ確保に関する問題のように復帰で きないエラーを示します。スクリプトの実行は中断されます。」(マニュアル)とあるように、捕捉する前に終了してしまいます(実験で確認しました)。
ということですので、色々工夫の余地はあるとは思いますが、E_WARNINGレベルのエラーは捕捉するべきだと思います。

※ $e->getMessage()の箇所にHTMLエスケープが抜けていたので追記しました(2015/4/26)

[PR]
「パーフェクトPHP+徳丸本セットを抽選で1名に差し上げちゃうキャンペーン」やってます
「安全なWebアプリケーションの作り方」電子書籍版販売しています。電子版はこちら。

PHP5.4.0でheader関数の脆弱性が修正された

PHPのheader関数にはHTTPヘッダインジェクション脆弱性がありましたが、PHP5.4.0で修正されていることを確認しましたので報告します。

PHPのheader関数はHTTPレスポンスヘッダを送信するための関数です。元々header関数には改行文字のチェックが入っていなかったので、HTTPヘッダインジェクション脆弱性が入りやすかったのですが、PHP4.4.2 および 5.1.2の修正として、「この関数は一度に複数のヘッダを送信できないようになりました。 これは、ヘッダインジェクション攻撃への対策です。」と、改行文字が入っている場合、レスポンスヘッダを送信しないようになりました(header関数のマニュアル参照)。

しかし、このチェックはラインフィード(0x0A)しかチェックしておらず、キャリッジリターン(0x0D)のみを使ったHTTPヘッダインジェクション攻撃が可能な状態でした。この状況は、拙著「体系的に学ぶ 安全なWebアプリケーションの作り方」のP205にコラム「PHPのheader 関数はどこまで改行をチェックするか」として説明しています。

これに対して、PHP5.4.0で、header関数がキャリッジリターンもチェックするように修正されました。
https://bugs.php.net/bug.php?id=60227に改訂履歴があります。廣川類さんが担当してくださったのですね。報告者として私の名前もあります。廣川さんありがとうございました。
手元の環境で確認したところ、PHP5.3.10では変更なし(キャリッジリターンを許容)、PHP5.4.0ではキャリッジリターンを含むヘッダ全体が送信されなくなりました。

私の本では、header関数の呼び出し側で改行文字をチェックすることを推奨していますが、PHP5.4.0以降でその必要はなくなりました。しかし、まだまだPHP5.3を使うケースも多いと思うので、当面の間は、header関数の呼び出し側で改行文字をチェックした方がよいでしょう。

ところで、header関数は、どうやってアプリケーションにエラーを返すのでしょうか。マニュアルを見ると、header関数はvoid型で、返り値は「値を返しません」とあります。例外も発生しません(PHPの組み込み関数は原則的に例外を発生させません)。
これは酷い仕様だと思うのですが、header関数(およびその他の組み込み関数)で例外を発生させる方法はあります。これについては次のエントリで説明します。

[PR]
「パーフェクトPHP+徳丸本セットを抽選で1名に差し上げちゃうキャンペーン」やってます
「安全なWebアプリケーションの作り方」電子書籍版販売しています。電子版はこちら。

2012年4月3日火曜日

悪いサニタイズ、良い(?)サニタイズ、そして例外処理

先日のエントリ「処理開始後の例外処理では「サニタイズ」が有効な場合もある」は、素材の消化不足、私の表現の未熟等から、一部で誤解を招いてしまったようで申し訳ありません。アプローチを変えて、サニタイズについてもう一度考えてみたいと思います。結論から言えば、悪いサニタイズはあっても、「良いサニタイズ」はないと考えます。しかしながら、状況によっては妥協の産物としてサニタイズを使うことは、あり得ると考えます。

本稿で用いる「サニタイズ」の定義

サニタイズという用語は、歴史的に都合の良いように使われてきた歴史があり、あらためてネット検索して見ると、本当に多様な使われ方をしていると感じました。その様子は、高木浩光氏のブログ記事『「サニタイズ」という言葉はもう死んでいる』からも伺えます。

ここでは、議論の都合上、以下をサニタイズの定義として用いることにします。
サニタイズとは、
主にセキュリティ上の目的で、
処理対象の文字列から、
処理に支障のある記号類を、
除去、あるいは別の文字(マイナス記号など)に置き換えることであり、
処理の行われる箇所(入口か出口かなど)は問わないことにする。
この段階で「いや、サニタイズとはそういうものではない」という意見が噴出しそうですが、前のエントリでは、奥一穂氏と私の間の共通認識は上記のものだったと考えますし、元々サニタイズの厳密な定義などないと思われるので、本稿ではこの定義を用います。
まず、「悪いサニタイズ」について説明します。

悪い例1:入力時のサニタイズ

入力時に、「支障の出そうな文字」をまとめて削除等することがありますが、よくありません。恐らく、サニタイズという用語をもっとも狭義の定義がこの使い方で、以下の定義になると考えます。
外部から入力された文字列から、
処理に支障のある記号類を、
削除あるいは別の文字(マイナス記号など)に置き換えること
「処理に支障のある記号類」としては、 < > ' " ; \ % _ & ? { } ` @ | などが候補ですが、これらに限りません。
2005年くらいまでは、上記の手法が脆弱性対処の主流として盛んに使われてきました。日本独特の手法としては、「別の記号に置き換える」代わりに「いわゆる全角文字に置き換える」という手法も用いられ、一種のサニタイズととらえることができます。
処理が簡便であるために人気のあった手法ですが、以下の問題があり、最近はあまり見かけなくなりました。
  • 脆弱性の発生メカニズムを根拠にしていないので、常に安全とは限らない
  • サニタイズ対象の記号をどう選んだらよいか根拠があいまいである
  • サニタイズ対象の記号を要件として使いたい時に困ってしまう
  • そもそも入力データをセキュリティ上の理由で勝手に改変してよいはずがない
ということで、現在では入力時のサニタイズは完全に否定されています。
しかしながら、例外として、大昔の「セキュリティをまったく考慮していないWebアプリケーション」を急いで脆弱性対処しなければならない時など、状況によってはこの方法をとらざるを得ないという場合もあるかもしれません。

悪い例2:入力時点でエスケープする

先の定義とは外れますが、入力データ時点でエスケープする手法もサニタイズと呼ばれることがあります。すなわち、操作内容ではなく、処理の場所に着目して、入力時点でセキュリティに必要な文字列処理を済ませてしまうことをサニタイズと呼ぶ場合もありますが、現在ではこの方法も否定されています。
その理由は、エスケープの方法は、データの使い方(HTML生成、SQL呼び出しなど)によって変わってくるため、入力時にあらかじめ済ませることは不可能だからです。
PHPのマジッククォートは、主にMySQLのSQL呼び出しやOSコマンドインジェクション対策を想定したエスケープを、入力時に自動的に済ませておく機能と捉えることが出来ます。しかし、脆弱性を完全に予防できるわけではなく、副作用が大きかったことから、PHP5.3で非推奨、PHP5.4では機能自体が削除されました。すなわち、入力時のエスケープがうまくいかないことは、歴史が証明しています。

悪い例3:エスケープ可能なのにサニタイズする

次に、入力時ではなくデータを使う時(出力時)に話題を移します。出力時に、記号類をエスケープする手段が提供されている場合(HTMLやSQLなど)はエスケープで対応すべきですが、サニタイズ(記号の削除など)で対処する場合があります。これも好ましくありません。
かつてEvernoteにXSS脆弱性が指摘された際、EvernoteのXSS対策として、一部の記号(「<」、「>」など)が削除されていました。このあたりの経緯は、ブログ記事「Evernote XSS事件のエクスプロイトとその対策過程と顛末」に詳しく報告されています。記事にあるように、「\」のエスケープが漏れていたためにXSS脆弱性が残ってしまいました。
出力時のサニタイズが悪い理由として以下があります。
  • 脆弱性混入の原理を把握しないで対処しているので漏れが生じやすい
  • サニタイズ対象の記号を使わなければならない場合に対応できない

次に、サニタイズの仕様が許容される(かもしれない)ケースを説明します。

許容例1:エスケープの手段がない場合

出力時はエスケープでの対処が基本ですが、エスケープ手段が提供されていない場合があり、その場合はサニタイズが対処の候補になります。
例えば、phpMyAdminのセットアップスクリプトには、利用者が入力した注記をPHPのコメントとして書き出す機能があります。たとえば、「1st server」という入力に対して、「/* 1st server */」と出力するという具合です。しかし、以下が入力されると、コメントが勝手に閉じられてしまい、スクリプトの注入が可能になります。
*/ 任意のPHPコード; /*
出力されるコメントは以下となり、外部から任意のスクリプトが注入できます。
/* */ 任意のPHPコード; /* */
これに対して、PHPのコメント機能には、「*/」をエスケープする手段が提供されていないので、phpMyAdminは「*」を「-」に変換することで、スクリプトの注入を防いでいます。「サニタイズ」後のコメントは以下となり、スクリプトの注入を防止します。
/* -/ 任意のPHPコード; /- */
このあたりの詳細については、私のブログ記事「phpMyAdminにおける任意スクリプト実行可能な脆弱性の検証」を参照ください。

しかし、やむを得ないとは書きましたが、好ましいわけではありません。そもそも、利用者の入力をPHPのコメントとして残すという仕様があぶなっかしい感じですし、それ以前に設定ファイルをPHPのスクリプトとして生成するという仕様も、セキュリティ上の問題が発生しやすいので好ましくないと考えます。

許容例2:表示できない文字を扱う場合

次にありそうなケースとして、処理できない文字が何らかの原因で入ってしまったケースです。ありそうなケースとしては、以下があります。
  • PCとケータイ(ガラケー)の両方に対応しているサイトで、ケータイでは表示できない文字を表示する場合
このような場合、表示できない文字を「〓」(いわゆるゲタ)等の代替文字で置き換えることをします。一般的に、表示できない文字を代替文字に置き換える処理を「サニタイズ」とは呼ばない気がしますが、冒頭の私の定義には該当すると考えます。

許容例3:予防的対策の一環として

今までの例は、エスケープできない種類のデータ形式で、かつその文字を仕様として許容している場合でしたが、このような例は希であって、通常エスケープ手段が提供されていない場合は、その文字は仕様として拒絶しなければなりません。そのような例として以下があります。
  • メールの宛先やタイトル欄の改行文字
  • リダイレクト先URLの改行文字
  • 数値項目中の数値以外の文字
これらは全てプログラムの入口でバリデーションとして入力値をチェックして、適切なエラーメッセージや再入力への誘導をすべきです。しかし、必要なバリデーション処理が抜けてしまったというケースはあり得ます。
このように、受け付けてはいけない(受け付けないはずの)文字が混入してしまった場合、即座に停止するのではなく、対象文字をサニタイズしてでも処理を進めた方が良い場合もある、というのが前回エントリの結論でした。

サニタイズは例外処理後の対処の一方法

これに対して、何人かの方から、サニタイズというのは例外を捕捉した後の対処の方法として捉えるべきだという指摘をいただきました。「受け入れてはいけない文字を受け入れてしまったので処理を継続できない」という状況は、確かに例外処理として扱うべき問題ですし、先のエントリで引用したfacebook上の会話にも、ブログタイトルにも例外処理という言葉は出て来ていたのに、そこを詰め切れていませんでした。
ここで、例外処理を一般化すると、以下のようになると考えます。
  • 例外が発生するかもしれないことを宣言する(try)
  • 処理実行中に例外の発生条件を検知する(数値以外の文字が…など)
  • 例外を発生させる(throw)
  • 例外を捕捉する(catch)
  • 必要な後始末をする(SQLの呼び出しをやめロールバックする…など)
  • try文を終了する
例外処理としてのサニタイズは、本来上記のようにしっかり例外処理を構成しなければならないところの、簡便法としてとらえることができます。
先に許容例2で挙げた「表示できない文字」についても、PerlやRubyのencodeメソッドは、対象外文字を例外として扱えるようになっていて、オプション設定で自動的に代替文字を表示できるようにもなっています。すなわち、例外時のサニタイズは、
  • 本来例外を発生させるべきだが、煩雑な場合もあるので、自動例外処理として代替文字への置き換えができるようになっている
ととらえることができます。

方式設計時に例外処理の方針を固めよう

先のエントリでは、処理が途中まで進んでしまった状況で、「受け入れてはいけない文字を受け入れていることが判明した」場合について、単に処理を打ち切るのではなく、サニタイズしてでも処理を継続した方がよい場合があると書きました。これは、例外処理という観点からは、
  • 受け入れ不可能な文字があるという例外が発生した
  • 例外処理として、受け入れ不可能な文字を削除して処理を継続する
ということになります。しかし、この方法がベストとは限りません。むしろ、「即座に終了よりはマシな簡便法」と言えるでしょう。一番まずいのは、「処理途中で受け入れ不可能な文字があれば、サニタイズして先にすすめばよいのだ」と決めつけてしまうことです。
そうではなく、処理の性質や使う言語の特性などを考慮して、例外発生時の処理方法を方式設計(アーキテクチャ設計)時に検討して、プロジェクトの標準として決めることが重要でしょう。
また、例外捕捉後の処理では、おもに「なかったことにする(ロールバック)」か、「辻褄を合わせて処理を継続する」かの選択になるかと思われますが、具体的な内容は、元々の処理内容に合わせてケースバイケースで決めるべきです。その際には、先に紹介した「例外処理の粒度」、「例外処理伸すコープ」という考え方も有効でしょう。

まとめ

このエントリの前半では、サニタイズという用語を仮に定義した上で、悪いサニタイズと、(良いとは言えないまでも)許容可能なサニタイズを例示しました。
後半では、「許容可能なサニタイズ」が、実は、「処理開始後に受け入れてはいけない文字が現れたという例外」であり、例外処理の処理形態の1つとしてとらえることができることを示しました。しかし、サニタイズが常にベストというわけではもちろんなく、処理をロールバックするか継続するかを含めて、アプリケーション要件と実装方針、開発言語の機能などから、例外処理の方針をきめるべきだというのが結論です。

補足:やはり、「サニタイズ」という言葉はもう死んでいる

このエントリを書くにあたってあらためて「サニタイズ」の用例を調べましたが、予想以上に意味の幅が広い状況でした。サニタイズとはエスケープのことだと説明しているエントリもあれば、入力時に一括して記号類を処理(エスケープも含めて)してしまうことをサニタイズと呼んでいる例もあります。佐名木さんの「セキュアWebプログラミングTips集」では、「バリデート+エスケープ=サニタイズ」という節があるくらいです(同書P126)。
このような状況では、仮に定義して用いたとしても、読者毎に「サニタイズの語感」が元々異なっている以上、誤解を招く危険性は非常に高いと言えます。
したがって、そもそも幅広い読者を想定したエントリでサニタイズという用語を用いてしまったこと自体が不用意でした。混乱を招くような用語を使い、申し訳ありません。私は、今後サニタイズという用語をできるだけ使わないようにしたいと思います。

2012年3月30日金曜日

処理開始後の例外処理では「サニタイズ」が有効な場合もある

このエントリでは、脆弱性対処における例外処理について、奥一穂氏(@kazuho)との会話から私が学んだことを共有いたします。セキュアプログラミングの心得として、異常が起これば直ちにプログラムを終了することが推奨される場合がありますが、必ずしもそうではないというのが結論です。

はじめに

Webアプリケーションの脆弱性対策では、脆弱性が発生するのはデータを使うところであるので、データを使う際の適切なエスケープ処理などで対処するのがよいと言われます。しかし、処理内容によってはエスケープができない場合もあり、その場合の対処についてはまだ定説がないと考えます。

エスケープができない場合の例としては、以下があります。
  • SQLの数値リテラルを構成する際に、入力に数値以外の文字が入っていた
  • メール送信しようとしたが、メールアドレスに改行文字が入っていた
  • 入力されたURLにリダイレクトしようとしたところ、URLに改行が入っていた

これらの場合、エスケープはできないので、以下のいずれかの対処をすることになります。
  • バリデーション:受け付けられない文字があった場合はエラーとして処理を終了する
  • サニタイズ:受け付けられない文字を削除するか、他の文字に置き換えて処理を継続する

ここで前提として、入力値のバリデーションはしているのだが、何らかの原因でバリデーション処理に漏れがあって、そのデータを使う(たとえばメール送信)際に、上記が分かったというシナリオです。つまり、本来上記のような異常値は来ない前提だが、セーフティネットとして、処理の段階で対処をしておくということです。

私は従来、このような場合はバリデーションがよいと考えていました。サニタイズは入力値を改変することになるので、いかなる場合でも好ましくないと考えていました。

奥一穂氏との会話

そこで、大垣さんの寄稿記事「なぜPHPアプリにセキュリティホールが多いのか? 第44回 セキュリティ対策が確実に実施されない2つの理由」に関連して、大垣さんの寄稿に肯定的なコメントとして、facebookで以下の投稿をしました。
徳丸 浩 出力時のバリデーションが必要な局面は確かにあって、(1)(好ましくはないが)SQLを動的に組み立てる際に、数値パラメータに数値以外の文字がある場合、(2)メール送信時に、メールアドレスや件名に改行が含まれていた、ようなケース。とくに(2)のケースは、メール送信用のラッパー関数を作って改行やメールアドレスの改行チェックをするとよい。本来は入力時のバリデーションで弾かれているはず(べき)だが、万一漏れていた場合の安全装置として出力時のバリデーションも組み込んでおくと効率よく安全性を高めることができる
これに対して、奥一穂氏から以下のコメントを頂戴しました。
Kazuho Oku 安全装置なら処理を停止よりはサニタイズすべきなのかなと思ったりします。昔、Twitter ライクなサービスが出力時バリデートしてて、ある人が不正な UTF-8 シーケンスを含むツイートを投稿した結果、多くのユーザーのタイムラインが表示 (ry
私はこの「事件」の詳細を知らないのですが、以下の現象だったと憶測しています。
  • 某Twitterライクなサービスはtwitterのように、フォローしている利用者のつぶやきが時系列的に表示される
  • ユーザのH氏が誤って(?)不正なUTF-8シーケンスを含むツイートを投稿した
  • そのつぶやきは入力時のバリデーションを通過してDBに格納された
  • そのつぶやきを表示する際に、不正UTF-8シーケンスが含まれているため、例外が発生した
  • 例外処理の結果、当該のツイート以降空となり、多くのユーザのタイムラインが影響を受けた

H氏が、自身の不正なツイートで不利益を受けるのは仕方ありませんが、他のユーザにまで影響が及ぶことは問題です。入力時のバリデーション不備が根本原因ではありますが、フェールセーフのつもりの例外処理があだとなって、影響が大きくなってしまいました。奥氏のコメントの意図は上記のようなものでしょう。

これに対する私のコメントと奥氏のリプライ。
徳丸 浩 奥さん、この問題を引き続き考えていたのですが、要約すると、処理が走り始めてから入力値に問題があったことがわかった場合、処理を中途半端に打ち切るのではなく、サニタイズしてでも最後まで走らせた方が安全(な場合が多い)ということでしょうか? 例外を発生させることとの選択になると思いますが
Kazuho Oku はい。そのように考えます。例えば、上のコメントにあげたケースは、処理を最後まで走らせずに例外をあげた結果、サービスにDoS脆弱性が発生していた、と理解すべき事象だと思います。
はせがわさん(@hasegawayosuke)も参入
Yosuke Hasegawa 「例外の粒度」的な観点ていうのはないんでしょうか。粒度というかスコープというのか。上の例でいえば、例外によって、不用意な発言した人の該当tweet *だけ* が消え去るのなら問題ないんでしょうけど、現実には広告を含め該当tweet以降のHTMLがすべて消えちゃったから問題になったわけで。
Kazuho Oku 不正なUTF-8シーケンスをうっかりtweetしちゃうとかハッカーこわいwww というのはさておき、「例外の粒度」という観点は同意です。ただ、例えばツイート単位で消しちゃうと、そのデータを UI から編集したり削除したりすることもできなくなるので、できるだけ小さな規模での「書き換え」(つまりサニタイズ)が望ましいと僕は考えます。
徳丸 浩 とても刺激的なお話です。便乗してもう一つお聞きしたいのですが、前提として、DB更新が途中まで走った後でもロールバックすればいいという教科書的な話に対して、現実の大規模な環境だと、ロールバックによって、きれいに「なかったことにする」ことも難しいのだろうと想像しているのですが、そういう理解であっていますか?
Kazuho Oku 現実には入力値検証を完了する前にDB更新を始めることはないでしょうから、脆弱性の話とはずれる気がしますが、大規模な環境だと「きれいに「なかったことにする」」のが難しいことは多いと思います(ただ、そのような回収もれのゴミはユーザーからは見えないようにするべきですが)。
徳丸 浩 ありがとうございます。前から気になっていました。そう言う前提がないと、(セキュリティはさておいたとしても)入力値検証を絶対やりなさいと言う動機付けという点で柔い気がしたものですから
上記のような具合に、私のウォールで豪華メンバーに参入いただき、楽しいディスカッションとなりました。

先のTwitterライクなサービスの場合で言うと、以下のような対処案が考えられます。

1.例外を発生させる(元々の仕様)

表示時に異常データがあれば処理を打ち切り、ファイルクローズなど必要な後始末の後プログラムを終了する。
この方法だと、先述のように、関係ないユーザまで被害を受けるので、よくありません。

2.当該のツイートのみ表示しない

異常なデータを含むツイートの単位で表示しないという方法です。
前項よりはよいですが、UIから異常ツイートを削除するのが不便であり、画面上も不自然な表示になる可能性があります。


3.異常データのみを削除するか、他の文字(「〓」など)に書き換えて表示する

(狭義の)サニタイズです。異常データの影響はもっとも軽微になります。
はせがわさんから指摘されていた「例外の粒度」はもっとも細かくなりますが、粒度が細かいことが好ましい例と言えます。

まとめ

結論は以下の通りです。
  • 処理が始まる前のチェック処理では、厳格なチェック処理と即座の停止をして良い(利用者に適切なメッセージを表示する前提で)
  • 処理(更新、表示など)が始まってから異常が発覚した場合は、後々の影響を考えた軟着陸をする必要がある
  • その際にサニタイズが有効な場合もある
サニタイズというと「サニタイズ言うなキャンペーン」を連想してしまい、「サニタイズは絶対にやってはいけない」と考える人もおられるような気がしますが、私たちはその段階をそろそろ卒業しなければならないように思います。「サニタイズ言うなキャンペーン」は、本質的な脆弱性対処を考える上で、いったん「サニタイズ」という用語を使わないで考えてみようという問いかけだったと理解していますが、本質的な脆弱性対処をした上でさらに例外時の処理にまで配慮する場合は、サニタイズが有効な場合もあるということです。

謝辞

上記はクローズドな場の議論でしたので、奥一穂さんとはせがわようすけさんに引用のご許可をいただきました。快諾いただきまして、ありがとうございます。

2012年3月11日日曜日

はてなブックマークボタンを外しました

この数日間問題になっている「はてなブックマークボタン」ですが、当日記およびHASHコンサルティングオフィシャルブログにも、当該ボタンがついていました。何が問題であるかは以下が詳しいですが、要は、はてなの管理下でない当サイトで、はてなのブログパーツが読者の皆様のトラッキングをしていることが問題です。

参考:
私は、2006年11月に、はてなダイアリーで日記を書き始めて以来、一貫してはてなのサービスを利用してきましたので、当ブログにも「はてなのボタンもつけとかなきゃな」程度のノリでボタンをつけておりました。その時点では、上記問題は知られておらず、また公式には始まってもいなかったようですので、知りようのないことではありましたが、それでも私の責任はあると認識しております。

私の本のP62には以下の記述があります。

第三者のJavaScriptを許可する場合
XSSは悪意の第三者によるJavaScript実行が問題でしたが、意図的に第三者のJavaScriptを実行させる場合があります。セキュリティ上の問題に対しては、サーバー運営者あるいは閲覧者が第三者を信頼する形で実行されます。

◆ サイト運営者が第三者を信頼して実行するJavaScript

サイト運営者が第三者の提供するJavaScriptを自サイトに埋め込む場合があります。典型的には、アクセス解析、バナー広告、ブログパーツなどです。このケースでは、サイト運営者が意図的にJavaScriptの提供元である第三者(以下提供元と表記)のJavaScriptを埋め込みます。
このように埋め込まれたJavaScriptに悪意があると、情報漏洩やサイト改ざんの危険性があります。このため、提供元が信頼できることが条件になりますが、実際には以下のような脅威があり、セキュリティ上の問題が何度も発生しています。
  • 提供元が意図的に個人情報を収集する
  • 提供元サーバーに脆弱性があり、JavaScriptが差し替えられる
  • 提供元のJavaScriptに脆弱性があり、別のスクリプトが実行させられる
バナー広告などのJavaScriptとXSSの結果動作するJavaScriptには、技術的に見れば同一の脅威があります。両者の違いはサイト運営者が提供元を信頼して意図的に埋め込んでいるかどうかです。従って、意図的に埋め込むJavaScriptについても、提供元の信頼性を十分調査した上で、保守的で慎重な判断が求められます。

例示の箇条書きのところで「(JavaScriptの)提供元が意図的に個人情報を収集する」という箇所があてはまります。引用部の最後の箇所「意図的に埋め込むJavaScriptについても、提供元の信頼性を十分調査した上で、保守的で慎重な判断が求められ」るにも関わらず、私自身が実行できておりませんでした。

という状況でしたので、当該のブックマークボタンは昨日撤去致しましたことを報告します。
また、はてなの日記の方でも、一時ついカッとなって類似のボタンを撤去しましたが、今は戻しています。その理由は以下の通りです。
  • はてなの管理下のサイトではこの問題はそもそも関係ない
  • はてなのサイトでは、 トラッキングはボタンとは別の方法で実装できるので ブックマークボタンだけを目の敵にしても意味がない

ですが、当面の間、はてなのサービス(ブックマーク、日記)は少なくとも新規の更新をやめようと思います。読者の皆様にはご不自由をお掛けしますが、代替として以下のサービスをご覧下さい。


以上、ご報告致します。

2012年2月20日月曜日

難読化していないAndroidアプリケーションは脆弱性か

このエントリでは、Androidアプリケーションにおいて、難読化が施されていない場合、脆弱性にあたるかについて議論します。

はじめに

Androidアプリケーションは主にJava言語で記述され、DEX形式のファイルにコンパイルされたコードを、DalvikというJava互換VM上で実行します。DEXおよびAPKファイルの仕様は公開されており、DEXにはクラスやメソッド等のシンボル名も含まれているため、リバースエンジニアリングが容易であると言われています。このため、Android SDKには標準でProGuardという難読化ツールが添付されています。
それでは、難読化の目的はそもそも何で、難読化でその目的は達成されるのでしょうか。

難読化の目的

Webアプリケーションの場合は、重要なロジックは主にサーバー側に存在するため、ソースコードを外部から取得することはできません。これに対して、スマートフォンアプリケーションの場合は、実行プログラムが手元の端末上にあるため、リバースエンジニアリングにより、(手間の大小はともかく)ソースコードを再現させることが可能です。
リバースエンジニアリングに対抗するために、プログラムを解析しにくくする行為を難読化と呼びます。
難読化の典型的な目的を以下に示します。

  • アプリケーションロジックの流出防止
  • 内部の隠れた脆弱性の発見防止
  • 課金コードの改変による不正利用の防止
  • アプリケーションの不正流用の防止
  • 暗号鍵など秘密情報の漏洩防止

ProGuardとは

ProGuardとは、Android SDKに標準で添付される難読化ツールです。ProGuardには複数の機能がありますか、難読化に寄与するものとしては、クラスやメソッドの名前の付け替え(連番のような形になる)があります。
クラス名やメソッド名から意味がわからなくなるため、リバースエンジニアリングがしにくくなることを期待しての機能でしょう。

ProGuardで十分なのか

セキュリティの専門家の立場から見ると、ProGuardの提供する機能は中途半端です。一般的に、リバースエンジニアリングというと、機械語のバイナリからの解析を指します。職業的にリバースエンジニアリングをしている人たち、例えばウイルス対策ソフトのマルウェア解析担当者は、なんのヒントもないマルウェアのバイナリを解析して、マルウェアの挙動を把握し、パターンファイルを作成しているわけです。最近のマルウェアは、単純な機械語ではなく、加えて難読化が施されていて解析が難しくなっていると言われています。
これに対して、ProGuardの「難読化」の結果は、機械語に比べるとはるかにリバースエンジニアリングが易しいと言えます。シンボルが除去されるとは言え、アプリケーションが利用するライブラリのシンボル名などは除去できないためです。
すなわち、ProGuardの効果は、「very easyをeasyにする」程度のものと言えます。このあたりの感覚については、杉山俊春氏の寄稿記事も参考になります。


商用難読化ツールはどうか

ProGuardはどうももの足りないということであれば、商用難読化ツールというものもあります。以下に、Android向けの商用難読化ツールを紹介します。
※いずれの製品も私自身は評価しておらず、推奨というわけではありません。

一例として、DashOの機能を見てみると、以下のような難読化機能が提供されています。
  • 改ざん検出と通知 
  • 名前の変更
  • 制御フローの難読化
  • 文字列の暗号化
  • バイトコードの最適化
  • ウォーターマーク(出所を追跡可能に)

このうち、名前の変更とバイトーコードの最適化はProGuardにもある機能ですが、これら以外はProGuardにはない機能です。
難読化というからには、この程度はやって欲しい気がしますが、商用難読化ツールは高価(DashOは最小構成で79万円)なため、手軽に導入するわけにはいきません。

ではどうするのがよいか

ProGuardでは頼りない、商用難読化ツールは高価だとすると、どうすればよいのでしょうか。
その答えは、先に紹介した杉山氏の記事タイトル『見せたくないなら「持たせない」が鉄則』だと思います。具体的には、見せたくないリソースやロジックは極力サーバー側に持たせ、スマートフォン側には、入力や表示など最低限の機能に絞ることです。
それでも、なんらかの事情により難読化したい場合はあるでしょうが、以下のように考えればよいでしょう。

  • リバースエンジニアリングによる想定被害金額を見積もる
  • 想定被害金額が商用難読化ツール購入費用よりも大幅に大きければ、商用難読化ツールにより難読化処理する
  • 想定被害金額が大きくなく、費用対効果が見合わない場合は、ProGuard+手動難読化を施す

手動難読化とは私の造語ですが、プログラムロジックを故意に複雑にすることで、リバースエンジニアリングの邪魔をすることです。冗長なコードを挿入したり、文字列をエンコードあるいは暗号化した状態で保持して、アプリケーション内でデコードあるいは復号するなどです(面倒です)。
手動難読化も、あまり期待しない方がよいとは思いますが、ソースコード診断をしていると「まるで難読化されたような」読むのが嫌になるソースコードに出くわすこともしばしばですので、プログラムの下手な人に依頼すると思わぬ効果があるかもしれません(冗談です)。

また、そもそも難読化はリバースエンジニアリングを難しくするものであり、不可能にするものではないので、リバースエンジニアリングによる影響は受容可能にしなければなりません。例えば、リバースエンジニアリングによって、大量の個人情報が漏洩するような設計はダメだと言うことです。


まとめ

以上をまとめると以下のようになります。
  • 難読化するか否かはアプリケーション要件である
  • 難読化が破られリバースエンジニアリングされた場合でも、影響が受容可能であるように設計する
  • 本当に難読化の必要な場合はProGuardでは機能不足であり商用難読化ツールを検討する必要がある
  • 商用難読化ツールは高価なので、導入にあたっては費用対効果の検討が不可欠

ということで、表題の「難読化していないAndroidアプリケーションは脆弱性か」という問の答えは、「一般には脆弱性とは言えず、アプリケーション要件である」が答えだと考えます。


[PR]
3月2日、株式会社DNPデジタルコム主催の「スマートフォン向けセキュリティセミナー」(五反田、無料)で基調講演します。この中で、スマートフォンアプリケーションのセキュリティに関する責任範囲や対策などについて、基礎的なところから説明できればと思います。

スマートフォンアプリケーションのセキュリティ強化策についての相談は、HASHコンサルティング株式会社まで。
「安全なWebアプリケーションの作り方」DRMフリーのPDFによる電子版もあります。

フォロワー

ブログ アーカイブ