Rails+Ruby 1.9では不正な文字エンコーディング攻撃で脆弱にならない理由を詳しく解説されたブログエントリをまっちゃさんのブログで知りました。 入力で特には何もしていない事は知っていたので、出力時のどこかで全体をチェックできるような仕組みになっているのでは?と思っていたのですが、ETagの値を生成するコードの正規表現で例外が発生する、という事だそうです。
なぜRuby 1.9でエラーが起きるのか
ActionController::Response#etag= メソッドの定義は下記のとおりです。
def etag=(etag)
if etag.blank?
headers.delete('ETag')
else
headers['ETag'] = %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(etag))}")
end
end調べると、このメソッドに渡される引数はレスポンスボディ文字列(HTML)でした。レスポンスボディがごっそり渡され、そのMD5ダイジェストがETagヘッダとして使われます(ActiveSupport::Cache.expand_cache_key は追っていません)。
さて、ここで etag.blank?、すなわち String#blank? の定義を見てみましょう。これは、active_support/core_ext/blank.rb によって追加されるメソッドです。
class String
def blank?
self !~ /\S/
end
end正規表現によるマッチングが行われています。なんと、ここで例外が発生するのです!(このRuby 1.9の挙動については、徳丸さんの前掲記事で指摘されているとおりです)
試しに:
class String
def blank?
self == ''
end
endとしたら例外は起きず、Ruby 1.8(パッチ適用前)のスクリーンショットと同じ状態になりました。
調べようと思っていたのですが、調べずに済みました :)
PHPで似た様な動作をさせる – あまりお勧めできないサニタイズ
PHPで、似た様な動作にしたい場合(ブラウザ以外からの入力も怪しいなど)出力時に強制的に文字エンコーディング変換してしまえば良いです。例えば、
mbstring.internal_encoding = utf-8
mbstring.http_output = utf-8
と出力文字エンコーディングを指定すると不正な文字列はサニタイズされます。この設定だけでは不十分なので実際に出力文字エンコーディングを変換したい方は追記のリンク先を参照して下さい。
ただし、http_outputをpass以外に設定すると副作用が発生するので注意が必要です。例えば、画像ファイルをUTF-8エンコーディングに変換すると確実に壊れます。
必要な文字が不正なマルチバイト文字によって消えてしまう可能性があります。これによりJavaScriptインジェクションが可能となる場合もあるので注意が必要です。
PHPで出力文字エンコーディングをバリデーションする – お勧め
PHPは出力バッファを利用してユーザ定義の出力バッファハンドラを設定できます。出力時にも文字エンコーディングバリデーションを導入したい場合、サニタイズするmbstring.http_output(これも内部的には出力バッファハンドラ)よりもユーザ定義の出力バッファハンドラを用いた方が良いでしょう。この方法であればリスクを伴うサニタイズでなくより安全なバリデーションが可能です。不正エンコーディングでエラーイベントを発生、バッファをクリアし、適切なエラーページを表示させる事ができます。
ユーザ定義の出力バッファハンドラの中身には
if (!mb_check_encoding($output)) {
trigger_error(‘Invalid char encoding detected’, E_USER_ERROR);
exit; // Make sure exit script!
}
などと記述し、ユーザ定義エラーハンドラでエラーを捕捉するようにします。例外を使いたい方は例外でも構いません。
この方法にも注意事項があります。PHPの出力バッファはネストさせる事が可能です。圧縮用のバッファは最後に適用される必要があります。圧縮したコンテンツの文字エンコーディングチェックをしても意味がありません。圧縮ハンドラを利用している場合、文字エンコーディングチェック用のハンドラは圧縮ハンドラの直前に適用するように調整します。それ以外は出来る限り最後になるように調整すると良いでしょう。詳しい出力バッファハンドラ、エラーハンドラ、例外の使い方はPHPマニュアルを参照してください。
アプリのコードを触りたく無い場合でもauto_prepend_file等をphp.iniから設定して、必要な出力ハンドラを登録してバリデーションを行い、適切なエラーを返すようにする事も可能です。イメージ出力などのスクリプトのみ出力エンコーディングのバリデーションをしないようにすればOKです。php.ini設定を行うにはphp.iniを使う方法、httpd.conf, .htaccessを使う方法、スクリプトから設定する方法など色々あります。こちらも詳しくはPHPマニュアルを参照して下さい。
入力と出力、両方でチェックできるなら両方でチェックする方がより安全です。動作を正しく理解した上で使えば問題も発生しません。PHPの場合、ほんの少しのコード/設定変更でかなり文字エンコーディング攻撃に強いアプリケーションになります。
おまけ
「出力でチェックできるなら、入力でのチェックは省略してもいいかな?」と考える方も居ると思います。これは多重のセキュリティを実装する上でお勧め出来ません。
出力時のバリデーションで自分が担当しているアプリの安全性を担保できても、それ以外のアプリはどうでしょうか?利用するデータ(DBMSも設定次第で不正な文字エンコーディングを含むデータが保存可能)やファイルへ不正な文字エンコーディングを含むデータが無いようにする為に入力時のバリデーションは欠かせません。
Rails+Ruby 1.9で不正な文字エンコーディング攻撃ができない理由を調べた岩本さんは「ますますRailsを使う気が失せました」と書かれています。私と同様にこのような状況を想定されているのでしょう。
不正な文字エンコーディングで出力時にエラーにするとDoSの問題も発生します。不正なユーザコメントでページが表示できなくなってもよい、というサイトは少ないでしょう。最後の手段として出力時に不正な文字エンコーディングを検出してエラーにして被害を抑える、という考え方で対処すべきです。
追記: 自分が作るユーザ定義エラーハンドラは必ず”exit”するのですが、他のエラーのハンドラの場合exitするとは限らないのでユーザ定義エラーハンドラでも大丈夫なようにexitを追加しました。本当は、エラーハンドラで全ての出力バッファをクリアし、適切なエラーページを表示する方法が一番好ましいです。多くの説明が必要なので省略しています。
新しく書いた http://blog.ohgaki.net/-27 には少し詳しく書いています。
追記:komuraさんが非常に良いフォローアップをされているので、こちらも参考にして下さい。出力時にも文字エンコーディングチェックした方が良い、という事だけ書きたかったのでかなり省略(ブログのサブタイトルの通り:) していますが、komuraさんのこのエントリを読めば具体的にどうすれば良いのか分かると思います。
http://d.hatena.ne.jp/t_komura/20090927/1254057044
しかし、コンテントタイプの事は書いている時は失念していました。komuraさんはmbstringの動作に非常に詳しい方なので助かります :)