「拡張子ではなく、内容によってファイルを開くこと」の拡張子は Content-Type ではないことに注意

少し前に JSONP が XSS を引き起こすかもしれないという点に関する興味深い記事を奥さんが書かれていました。

JSONP における Padding 部分(だけでなくJSON部分も。4/5追記)に攻撃者が HTML と解釈可能なスクリプトを注入することにより、JSONP なデータを直接 IE で開いた場合に HTML と解釈され XSS が発生する、という点について書かれています。
ここで、IE が JSONP を HTML と解釈する理由は以下の2点。

  1. IEのよく知られた機能「拡張子ではなく、内容によってファイルを開くこと」により、内容が HTML っぽい場合には、Content-Type: text/javascript が無視され HTML として解釈される。
  2. 上述の設定が「無効」に設定してある場合でも、URL path の拡張子と思しき部分を以ってファイルタイプを決定する可能性があること。

前者については説明するまでもなく、よく知られた話であると思います。後者については、IE ではレスポンスヘッダの Content-Type: がレジストリ HKCR\MIME\Database\Content Type 内に見つからない場合は、PATH_INFO (もしかすると QUERY_STRING も)に含まれる拡張子と思しき部分を利用してファイルタイプを決定します。例えば、次のような CGI <http://example.com/cgi-bin/dynscript.js> があったとします。変数 s の内容は、攻撃者がある程度*1設定できるものとします。

#!/bin/sh
echo "Content-Type: text/javascript"
echo ""
echo "var s = \"<html><script>alert(document.location);</script></html>\";"
echo "alert( s );"

この CGI に対して IE で http://example.com/cgi-bin/dynscript.js にアクセスした場合は、JScript ファイルを開くかどうかのダイアログが表示され、ユーザの確認を待って実行されます。ところが同じCGIに対し無用な PATH_INFO を付与しhttp://example.com/cgi-bin/dynscript.js/test.html としてアクセスした場合には、ファイルは HTML と解釈されるために、無条件に変数 s 内のスクリプト" alert(document.location) "が実行されてしまいます。
というわけで、text/javascript のような、IE が素直に解釈できない(前述したレジストリにエントリが存在しない) Content-Type: を吐き出すWebアプリケーションは注意が必要ということに、初めて気がついたのでした。

ちなみに、この問題への対応として、奥さんの記事では

1. padding に使用できる文字を制限する
2. json コンテンツ内の < をエスケープする
3. レスポンスヘッダで charset を指定する

ということを挙げています。これはコメント欄にも書かれているとおり

HTML として解釈されても問題ないようにするアプローチのほうが汎用的

という考えによるものだそうです。
逆に、HTMLとして解釈させないというアプローチとして

という手段もあります。

#!/bin/sh
echo "Content-Type: text/plain"
echo ""
printf "%512s" " "
echo ""
echo "var s = \"<html><script>alert(document.location);</script></html>\";"
echo "alert( s );"

もちろん、奥さんによる「HTMLと解釈されても安全に動作する」アプローチと、「HTMLと解釈されないようにするアプローチ」の両方を組み合わせることもできると思います。

*1:"や\などはエスケープされるとしましょう。その他の細かな話はとりあえず置いておきます。