BK通信 - ブラウザのバッドノウハウ コンテンツ編

最終更新日: 2008-11-21

WEB+DB PRESS Vol. 48 に向けて書いた記事の元の原稿です。


ソフトウェアなどを使いこなすために、ストレスを感じながらもしぶしぶ覚えなければならないようなノウハウ、「バッドノウハウ」がテーマの本連載、第5回の今回はブラウザのBK を、コンテンツの扱いに関連するものに絞って取り上げたいと思います。

IE の Content-Type sniffing

通常、ブラウザはHTTP のレスポンスの Content-Type ヘッダに応じて、コンテンツをどのように処理するか決めますが、Internet Explorer 7 (IE7) はこのヘッダを無視するときがあります*1

たとえば、次のようなファイルを test.txt という名前でウェブサーバに置いて、 IE7 からアクセスすると、サーバから Content-Type: text/plain (ただのテキストファイル) として送られてきているにも関わらず、 HTML として解釈されてしまいます。

#! /usr/bin/perl
print "<html><h1>hello, world</h1></html>"

同様の現象は text/plain に限らず、 image/jpeg などの画像の Content-Type が指定されているときにも起きます。この挙動は Content-Type sniffing と呼ばれています。

HTMLとして解釈された
HTMLとして解釈された

おそらく、この挙動は Content-Type が間違っていてもコンテンツを正しく表示したい、という意図のもとに実装されているのだと思われます。しかし、ウェブアプリケーションの脆弱性につながる恐れがある挙動としても知られています。

上の例では無害な HTML タグが使われているだけですが、<script> タグで JavaScript を埋め込むこともできます。これはユーザが任意のファイルをアップロードできるサイトで特に問題になります。テキストファイルや画像とみせかけて悪意のあるJavaScript をアップロードして他人に開かせる、という攻撃が可能であるためです。

ただのテキストファイルだと思ったのに...
ただのテキストファイルだと思ったのに...

この自動判別の挙動は IE8では X-Content-Type-Options: nosniff というヘッダを HTTP のレスポンスに入れることで無効にできます *2 。このような変な挙動は単に撤去すればいい気がしますが、この挙動に依存しているサイトを壊さないために互換性を維持しているのだと思います。

変な挙動でも互換性を維持!
変な挙動でも互換性を維持!

Content-Disposition ヘッダで解決?

上記の、本来の Content-Type と異なる形式でコンテンツが解釈されてしまうという問題を回避するには、ブラウザにファイルを開かせる代わりに保存させればいいのでは、というアイディアがあります。具体的には HTTPのレスポンスに

Content-Disposition: attachment; filename="foo.txt"

を加えます。このヘッダにより、ブラウザの中でコンテンツを開くのではなく、foo.txt というファイルとして保存せよ、とブラウザに伝えることができます。

しかし、IE には Content-Disposition ヘッダの扱いに過去に問題がいろいろあり *3、最近のバージョンでも問題が発見されているので *4、Content-Type sniffing を防ぐために Content-Disposition に頼るのは安全ではありません。

結局ダメなんかい!
結局ダメなんかい!

危険性のあるテキストファイル*5の内容をどうしても表示させたい場合は、Content-Type sniffing の影響を受けないように先頭の256バイトを空白で埋める *6、HTML の特別な文字をすべて &lt; &gt; などにエスケープした上で HTML として表示させる、といった方法があります。

簡単なはずのことが激しく難しい...
簡単なはずのことが激しく難しい...

ところで、 Content-Disposition というヘッダは元々、メールで使われている MIME というフォーマットを拡張するために導入されたものであり、正式には HTTP の仕様には含まれません。HTTP 1.1 の標準である RFC 2616 を見ると、Content-Disposition は HTTP の標準の一部ではないと明記されています *7。しかし、大半のブラウザは Content-Disposition ヘッダに対応しているので、 RFC 2616 の中でもこのヘッダの扱い方について言及されています。

標準じゃないのかよ!
標準じゃないのかよ!

日本語ファイル名の問題

Content-Disposition ヘッダの filename パラメータでファイル名を指定できると前述しましたが、では日本語のファイル名はどのように扱えばいいのでしょうか。これは実は奥が深い問題です。

単純に考えると、次のように日本語の文字をそのまま使えばいいような気がしますが、この場合、どの文字エンコーディングを使うかが問題になります。

Content-Disposition: attachment; filename="あいう.txt"

たとえば、 UTF-8 を使うと IE7 では「縺ゅ>縺・txt」のようにファイル名が文字化けしてしまいます。

やな予感がしてきた!
やな予感がしてきた!

日本語ファイル名とメールの世界

メールの世界でも添付ファイルの日本語ファイル名を扱うのはややこしい問題として知られています。多くのメーラーは次のようにエンコードします。

Content-Disposition: attachment;
 filename="=?UTF-8?B?44GC44GE44GGLnR4dA==?="

先頭の =?UTF-8?B? は文字エンコーディングが UTF-8 であること、 base64 でデータがエンコードされていることを意味しています。このようなエンコード方式は RFC 2047 で定義されており、 encoded-word と呼ばれています。

実は上の Content-Disposition は厳密には RFC 2047 に違反しています。RFC 2047 によるとencoded-word はダブルクオートで囲まれた文字列の中に含めるのは禁止させれているためです*8

メールの標準的に正しい方法は RFC 2231 に則った以下のような形式です。

Content-Disposition: attachment;
 filename*=UTF-8''%E3%81%82%E3%81%84%E3%81%86.txt

しかしこの「正しい」方法をサポートしていないメーラーもあるため、歴史的に、前述の「正しくない」方法の方が普及しています。

歴史的な事情かよ!
歴史的な事情かよ!

日本語ファイル名とウェブの世界

本題にもどって、ウェブの世界ではどうすべきかというと、これは厄介な問題です。以下の 6 パターンを調査してみました。

% + UTF-8

  filename=%E3%81%82%E3%81%84%E3%81%86.txt

% + Shift_JIS

  filename=%82%A0%82%A2%82%A4.txt

生UTF-8

  filename=あいう.txt

生Shift_JIS

  filename=あいう.txt

RFC 2231

  filename*=UTF-8''%E3%81%82%E3%81%84%E3%81%86.txt

RFC 2047

  filename="=?UTF-8?B?44GC44GE44GGLnR4dA==?="

結果は以下の通りです*9

|              | % + UTF-8 | % + SJIS | 生UTF-8 | 生SJIS | RFC 2231 | RFC 2047 |
| FF3 win      |     x     |     x    |    o    |   o    |    o     |    o     |
| FF3 mac      |     x     |     x    |    o    |   x    |    o     |    o     |
| IE7          |     o     |     x    |    x    |   o    |    x     |    x     |
| Chrome       |     o     |     x    |    x    |   o    |    x     |    o     |
| Safari3 mac  |     x     |     x    |    x    |   x    |    x     |    x     |

ひとまず、6パターン全滅している Safari を無視して考えれば、 Firefox のときは RFC 2231形式、 IE7 と Chrome のときはファイル名を UTF-8 でパーセントエンコードすればよさそうです。生の Shift_JIS を使うのは日本語ロカール以外のWindows が使われている可能性を考えると、避けたいところです(他の言語の Windows に Shift_JIS でファイル名を送ると文字化けします)。

ブラウザごとに全然違う問題!
ブラウザごとに全然違う問題!

IE7 が長いファイル名を切り詰める問題

ところが、パーセントエンコードには落とし穴がありました。

IE には一定の条件を満たすと長いファイル名を短く切り詰めるという仕様があり、パーセントエンコードを使った場合、この条件が比較的に簡単に満たされてしまいます。

たとえば、「日本語の長いファイル名をつけたいときもあります.txt」というファイル名を パーセントエンコードして、次のようなfilename パラメータを送ると、

filename=%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%AE%E9%95%B7%E3%81%84%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%90%8D%E3%82%92%E3%81%A4%E3%81%91%E3%81%9F%E3%81%84%E3%81%A8%E3%81%8D%E3%82%82%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99.txt

手元の環境では「%81%84ファイル名をつけたいときもあります.txt」というファイル名に切り詰められてしまいました。

切り詰められたの図
切り詰められたの図

よかれと思って長いファイル名つけたのに...
よかれと思って長いファイル名つけたのに...

一方、生の Shift_JIS を使って次のような filename パラメータを送った場合はファイル名は切り詰められません。

filename=日本語の長いファイル名をつけたいときもあります.txt

マイクロソフトのフォーラムの情報 *10 によると、 IE は「インターネット一時ファイル内のキャッシュ」のファイル名の長さに制限があり、パーセントエンコードを使うとこの制限を簡単に超えてしまうというのが問題のようです。

というわけで、長い日本語ファイル名を使いたい場合 (かつ日本語ロカールの Windowsを想定してもいい場合)、 IE には生の Shift_JIS を送るのがいいようです。

結局、生かよ!
結局、生かよ!

Mac の Safari 3 の対処法

前述の通り、Mac の Safari 3 では Content-Disposition ヘッダの filename を使って日本語のファイル名を指定するのは諦めるしかなさそうです。

ただし、まったく対処法がないわけではなく、Safari 3 は Content-Disposition に filename が含まれない場合は URL のパスの部分を元にファイル名を決めるので、Content-Disposition ヘッダには attachment パラメータだけを含め、次のようなURLにすれば「あいう.txt」を保存させることができます。

http://localhost/webapp/%E3%81%82%E3%81%84%E3%81%86.txt

残念ながら、次のように URLのクエリー部分 (?の後ろ) に指定しても無視されてしまいます。

http://localhost/webapp/foo.cgi?dummy=%E3%81%82%E3%81%84%E3%81%86.txt

ウェブアプリケーションの作り方によっては、 Safari 3 の問題を回避する URL を提供するのは難しいかもしれません。

やっとできたの図
やっとできたの図

そろそろ力尽きました...
そろそろ力尽きました...

まとめ

今回はコンテンツの扱いに関連するブラウザのバッドノウハウを紹介しました。ブラウザでファイルをダウンロードするなど簡単なことに思えますが、実は膨大なる BK が必要な高度な技であることがわかりました。特に、日本語ファイル名の扱いはウェブの時代になっても相変わらずややこしく、永遠のBKテーマなのではないかと思います。

日本語ファイル名は永遠のBKテーマなり
日本語ファイル名は永遠のBKテーマなり


*1IE 7.0.5730.13 で動作検証しました。IE6 でもおそらく同様ですが、手元に動作検証できる環境がないため今回は IE7 に絞って話を進めます。
*2 <http://blogs.msdn.com/ie/archive/2008/09/02/ie8-security-part-vi-beta-2-update.aspx>
*3たとえば <http://support.microsoft.com/kb/267991> によると、一部の IE のバージョンではテキストやHTMLの場合、Content-Disposition: attachment が指定されていても、ブラウザ内で表示してしまったようです。
*4JavaScript の history.back() と組み合わせる攻撃法が知られています。
*5ユーザがアップロードしたファイルなど
*6<http://msdn.microsoft.com/en-us/library/ms775147(VS.85).aspx> によると IEはコンテンツの先頭の256バイトを種類を判別します。
*715.5 Content-Disposition Issues より
*85. Use of encoded-words in message headers の An 'encoded-word' MUST NOT appear within a 'quoted-string' より
*9正確なブラウザのバージョンは Firefox 3.0.1 (Windows), Firefox 3.0.3 (Mac), IE7 7.0.5730.13, Chrome 0.3.154.9 (Windows), Safari 3.1.2 (Mac) です。
*10<http://forums.microsoft.com/TechNet-JA/ShowPost.aspx?PostID=3368207&SiteID=36>