つい最近CGIとクライアントサイドJavaScript絡みでHTMLエスケープの要不要で他人への説明に苦労した。それから少し経って別件でHTMLエスケープ絡みで話す機会があった。
同じようなことを2度も説明する機会があったということはつまり「資料にまとめなさい」という天の声があったに等しいと思うので、ここにまとめておくことにする。
なお私はよく分からない部分を妄想で補う人なので、以下の内容には多分に妄想が含まれているはず。有識者のツッコミ求む。
本エントリでの説明に用いる、ブラウザの動作モデル
説明し易くするため、ブラウザが以下のような感じの動作モデルで動いていると仮定する。ブラウザのソースコードを読んだことはないので、妄想率100%な代物だ。
+---------------------------------------------+ | WWWブラウザ | | | | +------------+ +------------+ | | +---->| JavaScript |--->| JavaScript | | | | | パーサ | | プロセス | | | | +------------+ +---+----+---+ | +------+ | | | | | | HTML | | | +---------------------+----+---+ | | |--+-----+ | DOM | | | 文書 | | | +---------------------+----+---+ | +------+ | | | | | | | +------------+ +---+----+---+ | | +---->| HTML |--->| HTML | | | | パーサ | | 抽象構文木 | | | +------------+ +---+----+---+ | | | | | +---------------------------------+----+------+ | | +---------------------+----+------+ | UI表示・ユーザ入力(フォーム等)| +---------------------------------+
HTML文書がブラウザに読み込まれると、まずHTMLとJavaScriptに分割されるとする。実際にはキレイに分けることなどできないはず*1だけど、便宜上そうなっていると仮定する。
JavaScriptのコードはJavaScriptパーサが解析し、その結果としてJavaScriptのプロセスが生成されるとする。ここでのプロセスはOSのそれではなく「もっと抽象的な何か」とする。う〜ん、計算実体とでもいうのか? うまい名前が思いつかない。
HTMLのコードはHTMLパーサが解析し、抽象構文木を生成する。これも厳密な意味での抽象構文木ではなく、ブラウザ内部で保持しているデータ構造ぐらいの意味だ。
JavaScriptからHTMLドキュメントを操作するにはDOMを経由する。DOMという抽象化層を経由してHTML抽象構文木を操作していると仮定する。
ユーザへの描画やフォーム入力等のユーザ操作もまた抽象構文木を操作していると仮定する。
HTMLのタグや文字実体参照はどこで解釈されるか?
HTMLのタグや文字実体参照はHTMLパーサが解釈し、解釈した結果として抽象構文木が生成されると仮定する。
なので例えばHTML文書中に「<」と書かれていたら、それはHTMLパーサによる解析中に「<」に変換される。抽象構文木の段階では変換後の「<」という文字になっている。
サーバサイド(CGI)でのHTMLエスケープ
サーバサイドでのエスケープ処理は、まず「何を生成するか?」という点に尽きる。
JSONならJSONとして正しいフォーマットになるように、CSVならCSVとして正しいフォーマットになるようにしなくてはならない。CSVとJSONはフォーマットが異なるので、必要となるエスケープの内容も違ってくる。
当然ながらHTMLを生成するのならHTMLとして正しいフォーマットになるようにしなくてはならない。それは「HTMLパーサやJavaScriptパーサによって解釈される」という点を念頭におく、ということだ。
生成するのがHTMLで、かつHTMLパーサが特別扱いする文字が含まれているけど特別扱いされたくないのなら、HTMLエスケープしなくてはならない。
これが、生成するのがJavaScriptならばHTMLエスケープではなくJavaScript用のエスケープが必要となる。その上で、scriptタグの要素として記述するのか任意のタグの属性値として記述するかによって追加の処理が必要となる。
この辺りは『体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践』でも読むべし。
クライアントサイド(JavaScript)でのHTMLエスケープ
JavaScriptはDOM経由で抽象構文木を直接操作する。ここでは恐らく生のデータでやり取りしているのだろう。
DOM経由での操作は基本的にHTMLパーサを介さない。それは例えばDOM経由で何らかの属性値となる文字列を追加する時、その中にHTMLとして特別な意味を持つ文字や記号が含まれていても何の効果も無い、ということだ。つまりHTMLエスケープは不要だ。
但しごく稀にHTMLエスケープが必要な場合もある。例えば以下のメソッドやプロパティを使う場合だ(他にも存在するかもしれない)。
- HTMLDocument.write()
- HTMLDocument.writeln()
- HTMLElement.innerHTML
上記のメソッドやプロパティ(代入する場合)は例外的に、入力された文字列を「HTMLのフォーマットに従ったもの」として取り扱う。効果は全く違うがJavaScriptでいうeval()のような感じだ(eval()は引数の文字列を「JavaScriptのフォーマットに従ったもの」として扱う)。
これはつまり、DOM経由でも例外的にHTMLパーサを介するパスがあるということだ。HTMLパーサを通過する以上、必要に応じてHTMLエスケープしなくてはならない。
まとめ
*1:例えば任意のタグの属性値としてJavaScriptのコードを埋め込むことができるし、またHTML文書中にscriptタグでdocument.write()を呼び出すコードを挟み込むこともできるので。