WASF Times版「サニタイズ言うな!」 by 高木浩光@自宅の日記 に 終わりなきサニタイズ言うなキャンペーン というトラックバックがついている。
このエントリを読むとエスケープという処理が如何に理解されていないか、ということが見えてくる。
誤解1. エスケープによってサーバに帰ってきたときに結果が変化すると思いこむ
「テキスト」として表示される情報は表示された時点で役目を終えますが、<input>や<textarea>の情報はサーバに返ってくるものなので、変更せずに返された場合は可能な限り元の情報と同じとなるようなエスケープ処理を施したい。
サーバに帰ってくるデータは、ブラウザ (ユーザエージェント) がHTMLを解釈し、アンエスケープした後のデータである。例を出そう。
| HTML: |
<form action="/some/where" method="get"> <input type="text" name="foo" value="ABC"> <input type="text" name="bar" value="ABC"> </form> |
| 結果: | GET /some/where?foo=ABC&bar=ABC HTTP/1.1 |
| HTML: |
<a href="/some/where?foo=ABC&bar=ABC">blah blah</a> |
| 結果: | GET /some/where?foo=ABC&bar=ABC HTTP/1.1 |
HTMLエスケープはたかだか、出力したHTMLを解釈する時点において効果を発揮するに過ぎない。HTMLパーサはエスケープを入力時に正しくアンエスケープする (データ入力時のアンエスケープ行為)。ユーザエージェントが出力する際に、アンエスケープ後のデータを出力するメソッド (フォームの場合はURLエンコードパーセントエンコーディング) で正しくエスケープする。
鳩丸ご意見番 – URI の中に & を書くべからず も参考にして欲しい (この記事も若干誤解を招くタイトルだが)。
誤解2. 見た目が同じならば解釈はいつも同じだと思ってしまう
「テキスト」や<textarea>は一部のタグや改行コードなどの制御記号を許容したり改行→<br>タグへの変換を行う事もありますが、<input>タグでは制御コードもタグも許容しない(<input>でタグを許容する事ってあるのかな?)。
テキストに一部のタグを許容、するのはまだ良いのだが、textareaや果てはinput要素にまで「タグを許容」云々の話が出てくるのは、完全に間違いである。
- textearea要素はその内容に一切のタグを書く (要素を入れる) ことはできない。内容はただのテキストデータである。
- input要素のvalue属性 (というより全ての要素の全ての属性) は、HTMLにとってただのテキストデータであって、「タグ」という存在が入り込む余地がない。
- ただし、スペース・改行・タブ以外の制御コードを排除するサニタイズを施すことは、たいてい無害であり、時には有用である。1
たとえば、 <input type="text" value="<br>"> というHTML断片は、文法的に正しいHTMLであるが、このinput要素のvalue属性には <br> という文字列が入っているに過ぎない。これはbrタグではない。これは <input type="text" value="<br>"> と記述した場合と完全に等価である。等価でないならば、そのHTMLパーサには根本的なバグがあるから捨てた方がよい。
もっとも、上の例のようなタグはたとえ文法的に正しくても、書くべきではない。属性値ではただしくHTMLエスケープを施し、 <input type="text" value="<br>"> と書くべきである。
誤解3. JavaScriptから読むとタグが解釈されると思っている
<textarea>や<input>タグは、リッチ入力フィールドなどの実装によりJavascriptからも読まれるため、可能な限り記号エスケープは避けたいかも。
誤解2.でも述べたが、textareaの内容や、要素の属性は「単なるテキストデータ」である。これはスクリプトから読もうが同じだ。
ここで注意すべきは、スクリプトがdocument.writeや element.innerHTML等でHTMLとして出力するデータは、HTMLとして出力時のエスケープ処理の対象であるということである。上の例にあるinput要素からvalueの値を取り出し (ここではまだ文字列 "<br>")、他の要素の innerHTML プロパティに代入したとしよう。ここで <br> が初めて「タグ」として解釈される。このときに適切にエスケープする責任はスクリプトにある。
なぜか。リッチテキストをスクリプトで実現するならば、ユーザが危険なタグを自ら入力する際も、適切にエスケープする責任がスクリプトにあるからである。2
結論
基本を守る、つまり……
- 入力時にアンエスケープを施す。これはライブラリやフレームワークが自動的に行ってくれる場合が多く、また任せるべきである。3
- 出力時に、出力に合わせた方法でエスケープする。
- SQLを組み立てる場合のように、内部の処理であっても、そのプログラムにとってSQLという出力であるということを忘れない。
- HTMLもそうだが、文脈によってエスケープの方法が変わることがある。たとえば、URLのクエリ文字列にデータを埋め込む場合は
URLエンコードパーセントエンコーディングを施す必要がある等。
- またはエスケープしなくて良い出力方法を選択する。
- DOMであればcreateTextNodeを使う
- snprintfではなく、strncpyを使う。fprintfではなく、fputsやfwriteを使う。
これらを守っていれば、100%データは安全である。
高木氏のエントリではCSRFについて触れられているが、CSRFではデータの運搬フローが脆弱なだけで、データの意味を途中で改竄されてはいないことに注目したい。
追記 (2007-02-09)
RFC 3986 で %xx のようなURIで使うエンコーディングは パーセントエンコーディング と呼ぶと決まっていたことを思い出したので、訂正した。
1 ISO-2022-JPのようなステートフルな文字エンコーディングを使っている場合、エスケープシーケンスを除去しないようにしなければならない。コードを複雑にしないために、ステートフルな文字エンコーディングをWebアプリケーションでは使用しないことをお薦めする。
2 リッチテキストのフォーマット処理をAjaxでサーバ側に委任することも最近は多いと思う。この場合は当然、適切なエスケープの責任はAjaxリクエストを受け入れるサーバ側にある。
3 ただしphpのmagic_quoteは典型的な入力時サニタイズであり、有害である。使ってはならない。