水無月ばけらのえび日記

bakera.jp > 水無月ばけらのえび日記 > 2008年のえび日記 > 2008年1月 > 2008年1月23日(水曜日)

2008年1月23日(水曜日)

JavaScript のリテラルに任意の文字列を出力してみる

更新: 2008年1月27日

Web アプリケーションのセキュリティガイドラインには、たいていの場合「スクリプト内に動的生成の文字列などを出力してはならない」という掟があったりするのですが、それにあえて背く場合のお話。

こんな感じの HTML 断片があったとします。

<script type="text/javascript">
<!--
foo.bar = <%= value %>;
//-->
</script>

そして、この「<%= value %>」の部分に、ユーザが検索文字列として入力した任意の値を入れるというのが課題です。もちろん、そのまま出力すると XSS であっさり死亡するので、何らかの処理をしてから出力する必要があります。ちなみに、ユーザが何を入力したのか正確に知りたいので、任意の文字を削除したりすることはできない (削除してしまうと仕様を満たせない) と考えてください。

要はエスケープ処理をすれば良いのですが、このエスケープ処理が凄い難易度です。たとえば、Ruby on Rails の場合だと escape_javascript というヘルパーが用意されているので、

foo.bar = '<%= escape_javascript(value) %>';

……と書けば良さそうに思うかもしれませんが、"</script><iframe……" などが渡されるとあっさり陥落します (おそらく、escape_javascript は外部 JS ファイル内での出力しか想定していない)。

文字列を to_json してしまうと良さそうにも思えますが、

foo.bar = <%= value.to_json %>;

実は to_json だと "--" がそのまま残ってしまうので、COM (コメント区切り子) がインジェクションできてしまいます。まあ script要素の内容モデルは CDATA なので COM が出力されても問題はないと言えばないのですが、

などでコメント区切り子が生きてくる可能性があるので、-- が出力されてしまうと問題があります。

と、そんなこんなでいろいろ考えたのですが、結局、全部 '\uXXXX' の形式 (ありみかさとみ先生 (www.remus.dti.ne.jp)によると「ユニコードエスケープ」と呼ばれたりするらしい) で出力することにしました。「Rails の to_json を 13 倍速くする方法 (d.hatena.ne.jp)」を参考に、こんな感じ。

def js_unicode_escape(text)
text.gsub(/([\x00-\x7f]|[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3})+/ux) { |s| s.unpack("U*").pack("n*").unpack("H*")[0].gsub(/.{4}/, '\\\\u\&') }
end

これで一切合切が (NUL文字さえも) \uXXXX の形式で出力されるようになったので、これを '' の中に突っ込みます。

foo.bar = '<%= js_unicode_escape(value) %>';

たとえば、「'"</script>-->」という文字列を投入すると、こんな感じで出力されます。

foo.bar = '\u0027\u0022\u003c\u002f\u0073\u0063\u0072\u0069\u0070\u0074\u003e\u002d\u002d\u003e';

これで、'' の中にはどうやっても \ と u と [0123456789abcdef] しか出現しないはずなので、まあ大丈夫かなぁと……。

※……と、思うのですが、それでも一抹の不安がぬぐえない。

※2008-01-27追記 : ちなみに、このコードは BMP にない文字に対応していません。本来は \x10000 以上の文字はサロゲートペアにして出力してやらないとダメです。

※2009-01-09追記 : サロゲート対応版もどうぞ。

関連する話題: セキュリティ / クロスサイトスクリプティング脆弱性 / Ruby / JavaScript

最近の日記

関わった本など