4.10. 再表示する可能性のある HTML や URI にはフィルタをかける

サイトにまたがった悪意あるコンテンツ(Cross-site Malicious Content)を防がなけ ればならない特徴的なケースの 1 つに、Web アプリケーションが挙げられます。 その Web アプリケーションは、あるユーザから HTML や XHTML を受け取り、それを 他のユーザに渡すように設計してあります(詳しい情報は Section 6.15 を見てください)。 下記のサブセクションでは、特にこの種の入力のフィルタリングについて論じます。 そういうケースを扱う必要性が当たり前になってきたからです。

4.10.1. HTML データを削除したり禁じたりする

(X)HTML タグをできるだけ削除すれば、最も安全になります。そうすれば、タグに よる影響は何も起こらず、また実現するのも比較的簡単です。 以前指摘したように、正しい文字の一覧を確認しているはずですから、その一覧 にない文字を拒否したり、削除したりできるはずです。 正しい文字の一覧に載っているからといって、単純にこのフィルタへ次の文字を 入れてはいけません。その文字とは「<」や「>」、「&」(属性に 使うなら二重引用符の「"」も)です。 ブラウザが HTML の仕様に従って動作するだけなら、「>」は削除する必要は ありません。しかし、実際は削除しなければいけません。 理由は、開始を示す「<」をそのページの著者が本当は置きたかった、と推測して いるブラウザがあるからです。この「手助け」が、攻撃者につけ込む余地を与えて、 「>」を使って「<」という望ましくない文字を作ってしまいます。

文字集合を HTML で送るには、通常 ISO-8859-1(国際化テキストを送る時でさえ) を使います。したがってフィルタは制御文字(改行やタブは普通は OK)のほとんど とハイビットにある文字も削除するべきです。

このやり方で問題になるものの 1 つは、国際化テキストを入力したユーザがその テキストが知らない内に消されてしまい、びっくりするという点です。 無効な文字が何の警告もなく削除されると、そのデータは完全になくなり、後になって 再構成のしようがありません。 選択肢の 1 つとして、そのような文字を禁止した上で、文字を使おうとしたユーザ にエラーメッセージを送り返してあげる方法があります。 少なくともこれでユーザに警告を出せますが、ユーザが望んでいる機能を提供でき るわけではありません。 その他には、そのデータをエンコードする方法と検証する方法が挙げられます。 これについては次に議論します。

4.10.2. HTML データをエンコードする

他にほぼ安全な方法として、危険な文字を変換してしまい、HTML 上で意味を 無くす、というものがあります。 すべての「<」を「&lt;」に、「>」を「&gt;」に、「&」を 「&amp;」にしてしまえばお終いです。 国際化文字ならどれも「&#value;」という形式を使って、Latin-1 に エンコードできます。最後のセミコロンを忘れないでください。 当然、入力エンコードをどうするかを理解していなければいければ、国際化 文字のエンコードはできません。

ここで考えられる危険には、エンコードした結果をたまたま 2 回処理すると 脆弱さが生まれてしまう、という現象が挙げられます。 しかしこのやり方では、少なくとも入力の「目的」が何であるのかを受けとった ユーザに伝えられます。

4.10.3. HTML データを検証する

アプリケーションがすべて機能する過程で、HTML を第三者から受け付けなければ ならず、その受け付けた内容を別のユーザに対して送る場合があります。これは 用心しなければいけません。今あなたは、とても危ない橋を渡っています。本当にこう することが必要なのか、自問自答してください。 あらゆるところから HTML を受け入れる、という考えでさえ、セキュリティに精通した 人々の間では賛否両論です。なぜなら、正しく取得するのは極めて困難だからです。

しかし、アプリケーションで HTML を受けざるを得ず、リスクを負うだけの価値がある と思うのなら、少なくとも HTML の「安全な」コマンドの一覧を確認 してから、そのコマンドだけを許可するようにしてください。

安全な HTML タグでアプリケーション(ゲストブックのような)にとって役に立つ ものを最低限ここにあげました。簡単なコメントをつけてあります。 <p> (パラグラフ)、 <b> (ボールド)、 <i> (イタリック)、 <em> (強調)、 <strong> (さらに強調)、 <pre> (事前に整形してあるテキスト), <br> (強制改行。閉じ用のタグは必要ありません) 上記に対応して終了タグもあります。

少数の「安全な」HTML コマンド群だけを受け入れるだけではなく、それら が入れ子になって閉じている(つまり、HTML コマンドが「対応がとれている」) ように必ずしてください。 XML では、これを「整形式(well-formed)」データと呼んでいます。 標準 HTML を許しているなら、多少例外があるでしょう(たとえば、<p> が出てくる前のところに </p> を想定するのは問題ないと思います)。 しかし、HTML ができる表現すべて(対応をとるための閉じ用タグが推測できる場合が 多い)を受け入れようとするのは、アプリケーション大半にとって必要ではありません。 もっとはっきり言うと、XHTML(HTML のかわりに) に忠実であろうとするなら、整形が 必要条件です。 また、HTML タグは大文字、小文字を区別しません。タグは大文字でも、小文字でも、 混ぜて使ってもかまいません。 しかし、XHTML を受け入れるつもりなら、タグはすべて小文字にしなければいけま せん(XML は大文字、小文字を区別します。XHTML は XML を使い、タグが小文字で あることが必要です)。

ここでいくつか TIPS を順不同であげておきます。 通常は、HTML テキスト及び許可すべきタグの集合に関する何かしらの設計を行なった 方が良いでしょう。 そうすれば、投稿されたテキストが「メイン」サイトのテキストとして誤って処理 されなくなります(偽造を防ぎます)。 どんな属性も、その属性タイプや値をチェックすることなしに受け入れてはいけ ません。 Javascript のように、ユーザをトラブルに巻き込む恐れがある属性がたくさん あります。それらの属性をサポートする必要があります。 上記の一覧には、属性がまったく存在しないことに注目してください。これが安全、 確実な方法なのです。 安全ではないタグが使われたなら、おそらく警告メッセージを出した方が良いでしょう。 しかしこれが現実的でないなら、危険な文字をエンコードして(たとえば「<」を 「&lt;」にする)、ユーザの安全を維持しつつ、データがなくなることは防いで ください。

4.10.4. ハイパーテキストリンク(URI や URL)を検証する

注意深い方ならお気づきだと思いますが、ハイパーテキストのリンクタグ <a> を安全な HTML タグとはしていません。 明らかに、<a href="safe uri">(ハイパーテキストリンク)を安全な一覧に 追加できたのにもかかわらずです(内容をチェックしない限り、他のどの属性も許可 しないこと)。 アプリケーションが必要としているなら、追加してもかまいません。 しかし第三者がリンクを張ることで、安全性がさらに低下します。理由は、 「安全な URI」の定義にあります。[1] これが結果的にはとても面倒事になります。 多くのブラウザは、ユーザにとって危険かもしれないあらゆる URI をすべて受け 取ってしまうからです。 このセクションでは、第三者からやって来て、他の人に再表示する URI の検証方法 と、その URI を HTML にどのように組み込むかについて論じます。

まず URI の文法をざっと見て行きましょう(さまざまな仕様書で定義してあるので)。 URI は「絶対」も「相対」も可能です。 絶対 URI はこのようになります。
scheme://authority[path][?query][#fragment]
URI はスキーム名(「http」のような)からはじまり、「://」、責任者部(authority)、 (「www.dwheeler.com」のように)、パス(ディレクトリ名やファイル名のような) と続きます。この後に疑問符を置いてクエリが続いたり、ハッシュ(「#」)を置いて フラグメント識別子が続いたりします。オプション部分は [] で囲んで あります。ただ現実には、クエリやフラグメントを含む URI は多くはありません。 スキームには、許可しないデータ(たとえばパスやクエリ、フラグメント)が ある一方、固有の条件を追加する場合もたくさんあります。 スキームは「責任者部」にオプションでユーザ名やパスワード、ポート番号を 許可している場合がよくあります。書き方は次の通りです。
 [username[:password]@]host[:portnumber]
「host」は名前(「www.dwheeler.com」)か IPv4 の数値形式のアドレス(127.0.0.1) を指定できます。 「相対」URI はあるオブジェクトを「現在の」オブジェクトからの相対位置で 参照し、その書き方はファイル名にとてもよく似ています。
path[?query][#fragment]
たいていの URI では、許可している文字数に制限があり、この問題を回避する ために 8 ビット文字を「URL エンコード」して %hh(hh には 8 ビット文字が 16 進の値で入ります) とします。 正しい URI について、さらに詳しい情報は IETF RFC 2396 とそれに関連した 規格書を見てください。

ここまで URI の書き方を見てきましたので、こんどはそれぞれの部分が持つ 危険性を調べてみましょう。

もちろん、単純さとのトレードオフもあります。 単純なパタンは理解しやすいのですが、正確だとは言えません(単純であるがゆえに あまりに甘いか、あまりにきついかのどちらかです。それが正確なパタンであったと しても)。 複雑なパタンはより正確になり得ますが、さらにエラーが起こったり、より性能が 必要となったりする恐れがあります。また環境によっては、実行するのが困難な場合 もありえます。

ここでは私の案として、「単純かつほとんど安全な」URI パタンを紹介します。 これは「手作業」で実行できる程単純で、正規表現を使っても可能です。 下記が許可するパタンです。
(http|ftp|https)://[-A-Za-z0-9._/]+

このパタンは潜在的に危険となる可能性のあるようなクエリやフラグメント、ポート、 相対 URI 等を認めず、わずかなスキームだけしか許可していません。 これは「%」文字の使用を防ぐことで、URL エスケープを避け、サーバがうまく扱え ないかもしれない文字を特定できるようになります。 また「:」や URL エスケープも許可していないので、ポートを指定するのも 認めていませし、より危険な URI へのリダイレクトも困難になります(エスケープ 文字が抜けているため)。 またその他多くの文字の利用も防ぎます。繰り返しますが、出来の良くない Web アプリケーションは「予想外の」文字をうまく扱えません。

この「ほとんど安全な」URI でさえ、疑わしい URI をいろいろと許可してしまいます。 疑わしいものとは、サブディレクトリ(「/」を利用して)や上位ディレクトリへの移動 (「..」を利用して)を試みるようなものです。 この手の不正なクエリは、サーバが検知すべきです。 不正なホスト ID(たとえば「20.20」)は許可してしまいますが、これがセキュリティ 上の弱点となったケースを私は知りません。 Web アプリケーションには、サブディレクトリをクエリのデータ(もっとひどい ものだと、コマンドのデータ)として扱うものもあります。これを防ぐのは一般的に 困難です。というのも、「お粗末な設計の Web アプリケーションすべて」を見つ けられる見込みは皆無だからです。 パスの使用制限は可能ですが、そうしてしまうとインターネット上の情報をほとんど 参照できなくなってしまいます。 またこのパタンでは、ローカルなサーバ上の情報(「http:///」や 「http://localhost/」、「http://127.0.0.1」を使って)は参照可能で、マシンの内部 ネットワークを使ってサーバにアクセスしています。 ここではサーバが、HTTP の GET 命令の結果を何かを動かす命令ではなく、単に情報 を取得する、という正しい解釈をするという前提に立たなければなりません。 Section 4.11 でこの点を推奨しています。 このパタンではクエリの書式を認めていませんので、ほんとんどの環境ではこれで 十分なはずです。

残念ながら、「ほとんど安全な」パタンが、まともで役に立つ URI も数多く防いで しまいます。 たとえば、Web サイトの多くは、「?」文字を特定のドキュメントを区別するのに 使用しています(たとえば news サイトでの記事)。 「#」文字はドキュメント中の特定のセクションを特定するのに役に立ちますし、 相対 URI を許可することで、議論が扱い易くなります。 さまざまな許可された文字や URL エスケープは「ほとんど安全な」パタンには 含まれていません。 たとえば、URL エスケープを許可しないと、英語以外のページにアクセスするのは 困難になります。 本当にそのような機能が必要なら、機能が上がるほどユーザのリスクも増えるという ことを認識した上で、安全性が低いパタンを使ってもかまいません。

クエリは許可するが、プロトコルやポートに制限をかけるパタンは 下記の通りです。私はこれを「単純でやや安全なパタン」と呼ぶことにします。
 (http|ftp|https)://[-A-Za-z0-9._]+(\/([A-Za-z0-9\-\_\.\!\~\*\'\(\)\%\?]+))*/?
このパタンは洗練されているわけではありません。不正なエスケープや複数のクエリ、 ftp でのクエリ等を認めているからです。 ただ比較的単純という長所は持っています。

現実には、「やや安全な」パタンの作成して、正しい値を持つ URI を制限するのは 非常に難しい作業です。 ここでは、現状私が試しているパタンである「手の込んだやや安全なパタン」 を載せてみます。空白は無視して、コメントは「#」で表示してあります。
 (
 (
  # Handle http, https, and relative URIs:
  ((https?://([A-Za-z0-9][A-Za-z0-9\-]*(\.[A-Za-z0-9][A-Za-z0-9\-]*)*\.?))|
    ([A-Za-z0-9\-\_\.\!\~\*\'\(\)]|(%[2-9A-Fa-f][0-9a-fA-F]))+)?
  ((/([A-Za-z0-9\-\_\.\!\~\*\'\(\)]|(%[2-9A-Fa-f][0-9a-fA-F]))+)*/?) # path
   (\?(                                                              # query:
       (([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+=
        ([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+
        (\&([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+=
         ([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+)*)
       |
       (([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+  # isindex
       )
   ))?
   (\#([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+)? # fragment
  )|
 # Handle ftp:
 (ftp://([A-Za-z0-9][A-Za-z0-9\-]*(\.[A-Za-z0-9][A-Za-z0-9\-]*)*\.?)
  ((/([A-Za-z0-9\-\_\.\!\~\*\'\(\)]|(%[2-9A-Fa-f][0-9a-fA-F]))+)*/?) # path
  (\#([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+)? # fragment
  )
 )

上記の手の込んだパタンでも、不正な URI すべてを禁止するわけではありません。 たとえば、繰り返しになりますが「20.20」は不正なドメイン名ですが、パタンを 通過してしまいます。しかし私の知るところでは、これによってセキュリティ上の 問題は発生しません。 手の込んだパタンは制御文字(たとえば %00 から %FF の範囲)を表す URL エス ケープを禁止しています。許可している最小のエスケープ値は、%20(ASCII の空白) です。 制御文字を禁止することで、トラブルはいくつか防げますが、制約もあります。 すべての「2-9」を「0-9」に変更することで、制御文字を任意の Web アプリケーション に送れるようになります。 このパタンはパスにおいて、これ以外すべての URL エスケープを許可しています。 国際化文字には便利ですが、国際化文字を扱えないシステムでは問題を起こします。 このパタンは少なくとも URI の中で、空白や改行、二重引用符、その他危ない文字を 防ぎます。これによって、その URI を作成済みのドキュメントに組み込んだ時に その他の種類の攻撃を防ぎます。 このパタンはあちこちで「+」を許可している点に注意してください。理由は、 プラスが現実には空白文字の代わりとして、クエリやフラグメントで使われている からです。

上記で述べたように、残念なことにクエリデータを許可すると、そのテクニックを 使った攻撃があり、またクエリを許可してしまうと、現実に防御がほうまくできない ように思えます。 そこで、上記のパタンからクエリデータを扱う機能を除いてしまうことも、やろうと 思えば可能です。やり方を変えて「手の込んだやや安全なパタン」を作成してみます。
 (
 (
  # Handle http, https, and relative URIs:
  ((https?://([A-Za-z0-9][A-Za-z0-9\-]*(\.[A-Za-z0-9][A-Za-z0-9\-]*)*\.?))|
    ([A-Za-z0-9\-\_\.\!\~\*\'\(\)]|(%[2-9A-Fa-f][0-9a-fA-F]))+)?
  ((/([A-Za-z0-9\-\_\.\!\~\*\'\(\)]|(%[2-9A-Fa-f][0-9a-fA-F]))+)*/?) # path
   (\#([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+)? # fragment
  )|
 # Handle ftp:
 (ftp://([A-Za-z0-9][A-Za-z0-9\-]*(\.[A-Za-z0-9][A-Za-z0-9\-]*)*\.?)
  ((/([A-Za-z0-9\-\_\.\!\~\*\'\(\)]|(%[2-9A-Fa-f][0-9a-fA-F]))+)*/?) # path
  (\#([A-Za-z0-9\-\_\.\!\~\*\'\(\)\+]|(%[2-9A-Fa-f][0-9a-fA-F]))+)? # fragment
  )
 )

今言えることは、これらのパタンがユーザが選択したハイパーテキストのアンカー (「<a>」タグ)だけをチェックしている限り、この方法で「Web のバグ」の 混入も防ぎます。 Web バグは単純なテキストで、メインページのある大元の Web サーバではない第三者 が、いつ誰がそのコンテンツを読んだか、というような情報を追跡できるように します。詳しい情報は、 Section 7.7 を見てください。 同じようなチェックルールで <img>(画像)タグに使っているなら、これは 当てはまりません。画像タグは即座にロードされ、誰かが「Web バグ」を追加できます。 くどいようですが、ここではどんな属性も許可していない、ということを前提にして います。危険な属性はとても多く、せっかく提供しようとしているセキュリティに 穴をあけてしまいます。

これらすべてのパタンは、URI がそのパタンに完全にマッチしていることが条件 になっていることをどうか忘れないでください。 このパタンで不満なところは、ある面、許容可能なパタンにも制限をかけてしまい、 便利なパタンの多くを禁じてしまうところです(たとえば新たな URI スキームの利用 を妨げます)。 また、Web サイトの中には 1 つのクエリを表わすのに、さらに多くのクエリが実行 されるところもあり、これを防ぐのは現実問題としてとても困難です。さらにその ような Web サイトには、全体構成に組み込まれてしまっているものもあります。 結果として、Web サイトが複数の GET クエリを 1 つの動作として受け取る限りは、 URI は本当に安全とは言えません(Section 4.11 参照)。 正しい URL や URI についてさらに情報が知りたければ、IETF RFC 2396 を 見てください。ドメイン名の書式については、IETF RFC 1034 で詳しく論じています。

4.10.5. その他の HTML タグ

さらに HTML タグをサポートするにはどうしたら、と考えても不思議はありません。 次に打つ手ははっきりしています。それはリスト形式のタグで、 <ol> (ordered list)や <ul> (unordered list)、<li> (list item)がその対象になります。 しかしあるところまで来てしまうと、実際すべての機能を許可していることになります (その場合、提供者を信頼するか、ここで説明した内容よりも厳しくチェックする 必要があります)。 それより重要なのは、追加した新しい機能はどれもが、エラー(もしくは攻撃しやすい ところ)になるとっかかりとなる点です。

例として、同じような URI パタンで <img>(画像)タグを許可する場合を挙げ ます。 許可することで、明らかに安全性が下がります。理由は、「Web バグ」の挿入を 第三者に許可してしまうからです。Web バグで、誰が、いつこのドキュメントを 読んだのかを特定できます。 Web バグを詳しく知りたければ Section 7.7 を見て ください。

4.10.6. 関連事項

Web アプリケーションは文字集合(普通は ISO-8859-1)を指定しなければいけません。 信頼できないユーザがデータが他の文字を使っていても、許可してはいけません。 Section 8.5 にさらに詳しい情報があります。

この種の入力をフィルタすると、間違いが起こりやすいので、他の手段も同様に 論じられてきました。 選択肢の 1 つは、別の言語を使うようにユーザへ確認を取るというもので、これは HTML よりも簡単に設計できます。 デザインする HTML がより単純になります。またその言語に対して機能に制限を かけられます。 もう 1 つの解決方法は、HTML を解析して、「安全な」内部形式に変換し、その 安全な形式を HTML に解釈し直す方法があります。

フィルタリングは入力時、出力時、もしくはその両方で実施できます。 CERT が推奨している方法は、データを出力の過程、つまり動的なページの一部 としてまさにレンダリングされる前でのフィルタです。 正しく実施できれば、このやり方で確実に動的なコンテンツをすべてフィルタ できます。 CERT は、入力側でのフィルタリングはあまり効果がでない、と考えています。 理由は、動的なコンテンツが HTTP という手段ではなく、 Web サイトを構成する データベースの一部になっているからです。そしてこの場合、Web サーバは入力処理 過程においてそのデータを扱いません。 フィルタリングが、動的なデータが入ってくるあらゆる場所で実行されない限り、 データの中身は汚染されたままになっているでしょう。

しかし、私はこの点に関して CERT に同意しかねます。 入力と同様、出力すべてに対して、うっかりフィルタをかけ忘れてしまう点に問題が あります。また「汚染された」データのシステムへの侵入を許すと、どこかで厄介ごと が起こるのを待つ羽目になります。 安全なプログラムは、入力すべてをフィルタしなければいけません。これらのチェック を入力フィルタの一部に入れる方が良い場合があるからです(そうすれば、メンテナー が、そのルールは本当にそうなっているのかを確認できるようになります)。 そして、安全が必要となるプログラムには、プログラムのあちこちで出力を行う箇所 があるものの、データがプログラムに入力される方法や場所はほんの数ヶ所になって いる場合があります。このようなケースでは、入力でフィルタリングする方法は優れた 方法になるでしょう。

Notes

[1]

技術的にはハイパーテキストのリンクは、「uniform resource identifier」(URI) といえます。 「Uniform Resource Locator」(URL)は、URI のサブセットとして使われ、まず アクセスする手段(たとえばネットワーク上の「位置」)を表示することで、リソース を特定しています。リソースの名称やその他の属性では特定しません。 「URL」を「URI」と同じ意味で使う場合が多いのですが、それは URI を実現して いる中で、一番使われているのが URL だからです。 たとえば、URI で使用するエンコードのことを実際は「URL エンコーディング」と 呼んでいます。