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)。
このような状況では、仮に定義して用いたとしても、読者毎に「サニタイズの語感」が元々異なっている以上、誤解を招く危険性は非常に高いと言えます。
したがって、そもそも幅広い読者を想定したエントリでサニタイズという用語を用いてしまったこと自体が不用意でした。混乱を招くような用語を使い、申し訳ありません。私は、今後サニタイズという用語をできるだけ使わないようにしたいと思います。

0 件のコメント:

コメントを投稿

フォロワー