XSS対策:どの文字をエスケープするべきなのか

ITproの連載中に、id:hasegawayosukeさんから以下のようなコメントをいただいていました。

対策遅らせるHTMLエンコーディングの「神話」:ITpro - 葉っぱ日記

全ての文字をエスケープしようなどと非現実的なことは言わないけれど(とはいえMicrosoft Anti-Cross Site Scripting Libraryのようにほとんど全ての文字を実体参照に置き換えるものもあるので、あながち非現実的とも言えないのかも知れない)、エスケープ対象を「'」「"」「<」「>」「&」の5文字に限定しているのは何かこの記事に書かれていない理由があるはずだと思ったからです。

はてぶの方は見ていましたが、日記の方を見落としていまして、お返事が遅れました(_ _)。

なぜ、「<」、「>」、「&」、「"」、「'」の5種類の文字をエスケープするのかについては、色々考えるところがあります。

その前に、金床さんからご指摘いただいたように、

金床 『わたしも最初はせがわさんと同じ勘違いをしましたw
本記事は「すべての項目についてHTMLエンコードする」といっているのであって
「すべての文字を」という意味ではないですね(たぶん)
神話1とか神話2とかはダメだよ、という意味でしょう。

そういう意味です。どうも紛らわしい表記をしてしまったようです。

それとは別のテーマとして、XSS対策としてどの文字をエスケープするのが正しいのか、ということですね。
まず、連載時にリファレンスとして意識していたのは、本文中にも何度か現れるように、「安全なウェブサイトの作り方 改訂第2版」です。同じIPAから出ているIPA ISEC セキュア・プログラミング講座に比べて内容が現代的だし、なんといっても執筆協力者の筆頭に高木浩光氏がのっていることからも、広く入手できる資料としてはもっとも信頼性が高いと思ったわけです。しかし、意見が異なる部分も当然ながらあるわけです。全部同意だったら、「安全なウェブサイトの作り方 改訂第2版」を読めで終わりですものね。

同書には、

エスケープ処理には、ウェブページの表示に影響する特別な記号文字(「<」、「>」、「&」など)を、HTML エンティティ文字(「&lt;」、「&gt;」、「&amp;」など)に置換する方法があります。また、HTML タグを出力する場合は、その属性値を必ず「"」(ダブルクォート)で括るようにします。そして、「"」で括られた属性値に含まれる「"」を、HTML エンティティ文字「&quot;」にエスケープします。

とあるように、

エスケープ対象項目 エスケープ対象文字
要素内容(一般のテキスト) 「<」、「>」、「&」
属性値 「<」、「>」、「&」、「"」

と推奨しています。
しかし、こうする理由は自明ではありません。

要素内容について言えば、タグとタグにはさまれた部分になるわけなので、ミクロに見れば

>○○○○○○○○○○○○<

ということで、終端をあらわす「<」と、エスケープに使用する「&」の二種類が必要最低限のエスケープ対象文字となります。既に、えむけいさんから以下のようにコメントがついているとおり、「>」をエスケープする理由は自明ではありません。

えむけい 『「>」のエスケープは(XSS対策上はともかくHTML/XMLの仕様的には)ほとんどの場合不要ですが、たいてい無条件にエスケープしますね。』

また、属性値については、

"●●●●●●●●●●●●"

となるので、終端を示す「"」と、エスケープに使用する「&」の二種類が必要最低限ということになります。ただし、W3CのHTML4.01の仕様を見ると、

5.3.2 Character entity references
【中略】
Similarly, authors should use "&gt;" (ASCII decimal 62) in text instead of ">" to avoid problems with older user agents that incorrectly perceive this as the end of a tag (tag close delimiter) when it appears in quoted attribute values.

とあるように、古いブラウザが属性値(「"」で囲ってあっても)中の「>」によりタグの終端とみなすという問題(バグでしょうね)を回避するために、属性中の「>」を&gt;にエスケープする「べきである」と記述されています。すなわち、最低限のエスケープだと以下のようになります。

エスケープ対象項目 エスケープ対象文字
要素内容(一般のテキスト) 「<」、「&」
属性値 「"」、「>」、「&」

ただし、引用したW3Cの部分の記述が属性値のみを指すのか、一般のテキスト(Element Content)をも指すのか曖昧であること、「<」と「>」の対称性から、普通は、要素内容中であっても「>」をエスケープするし、属性値の中であっても「<」をエスケープするのだと解釈しています。すると、以下のように、「安全なウェブサイトの作り方 改訂第2版」の推奨内容にたどり着きます。

エスケープ対象項目 エスケープ対象文字
要素内容(一般のテキスト) 「<」、「>」、「&」
属性値 「"」、「<」、「>」、「&」

私が、シングルクォート「'」もエスケープ対象に加えているのは、属性値をシングルクォートで囲むケースが少なからず見受けられるからで、かならずダブルクォート「"」で囲むように徹底されていれば、このガイドラインは必要ありません。また、要素内容中のクォート文字「"」、「'」もエスケープ対象として推奨しているのは、文字二種類の違いのためにわざわざ記述を分けるのも煩雑なので安全な方に寄せたらどうかという提案であって、テンプレートエンジンなどで自動的に区別がつくのであれば、クォート文字のエスケープは属性値のみでもいいかな、とも思います(連載で指摘したような半端な二バイト文字のケースはありますが)。

同業者の中には、非常にたくさんの特殊文字エスケープするように推奨しているところもあって、それはそれで意味がなくはないのですが、金床さんの指摘のように、

それとは関係なく全ての文字をエスケープするのはいいと思いますよ。
デバッグやりづらいですけどw』

デバッグなどがやりづらくなるのも問題なので、バランスを見て適当なところで線を引いています。デバッグがやりづらいということは、バグが残りやすいということであって、脆弱性(=一種のバグ)が残りやすいと言えなくもないですしね。