URI(URL)における非ASCII文字の憂鬱

目次

  1. 導入
  2. URIに非ASCII文字が含まれる場合
  3. URIをUTF-8でエスケープしている場合
  4. URIを記述したドキュメントの文字コードでエスケープしている場合
  5. URIに複数の文字コードが混在する場合
  6. もっとも良い解決策
  7. テストケースと、各ユーザエージェントの対応状況
  8. 参考

導入

URI(URL)には、%7Eといった形式でASCII以外の文字を扱えます (RFC 3986 2.1では、これをパーセントエンコーディングと記述していますが、 この文書ではエスケープと記述しています。ご注意ください)。 これは同時にあらゆるバイナリデータの列をURIに含めることができることを意味します。 つまり、アンエスケープを行ったURIはどのような仕様にも依存せず、単なるバイナリ列となることを意味します。

通常、利用者の側から考えるとこの事実は何の問題もありません。 しかし、URIを扱うアプリケーションがURIをできる限り人間に読みやすい形で表示しようとした場合に様々な問題が発生します。 そういった情報を少しだけここで紹介したいと思います。 Webページの作成者の方も是非、一度目を通しておいてもらえれればと思います。

URIに非ASCII文字が含まれる場合

Webページの作成者はURIをHTML/XHTML等に記述する場合、各種仕様から、 URIに非ASCII文字を(厳密には一部のASCII文字も)直接使えません。 しかし、HTML4.01の仕様書において、 URIを処理する側(例えばユーザエージェント(ブラウザ))はWebページがこのような仕様違反に対して次のように処理することを推奨しています。

こうした場合に非ASCII文字を扱うため、ユーザエージェントが次の規則に従うことを推奨する。

  1. 与えられた各文字を、UTF-8の1バイトあるいは複数バイトで表現する。
  2. URIのエスケープ機構により、このバイトをエスケープする。すなわち、各バイトをバイト値の十六進表現HHを用いて「%HH」で表す。

しかも、次のような注意書きも追記されています。

注意。古いユーザエージェントの中には、HTMLのURIを、当該文書の文字符号化方法をそのまま適用して処理するものもある。また、古いHTML文書の中にはこうした処理方法に依存しているため、符号変換に際して破綻するようなものもある。そうした古い文書を処理するユーザエージェントは、正当な文字集合の範囲にない文字を含むURIを受け取った場合、まず初めにUTF-8に基づく変換を試みねばならない。そして変換結果が巧く扱えないものである場合のみ、当該文書の文字符号化方法のバイトに基づくURIの変換を試みるようにしなければならない。

つまり、仕様違反の文書に対しても互換性のために、ユーザエージェントはなんとか互換性を保ちつつ、 処理を行うようにすることが要求されています。

もちろん、Webページの作者はこのような仕様違反を行うべきではありません。 そのURIが別文書、または動的ページへのパラメータを含むリンクだった場合、 UTF-8でエスケープされたURIが送信されるのか、リンク元文書の文字コードでエスケープされて送信されるかは、 ユーザエージェント、またはその利用者の設定に依存するためです。

例えば、Firefoxでは、この状況において、常にUTF-8でエンコードするというオプションがあり、 Firefox1.5リリース直前まではこのオプションが有効になっていました。 しかし、正常に機能しなくなるWebサイトがまだ多い、という理由でデフォルト設定はFirefox1.0と同じ、 オフに戻されました。

しかし、そのURIを(例えばステータスバーやツールチップを利用して)ユーザに示す場合、 実は最も簡単、確実なURI記述形式であるという興味深い事実があります。 なぜなら、そのURIをそのまま、なんの追加処理も行うことなく表示することで、文字列として、 つまり最もユーザに読みやすい形で表示を行うことができるのです。

重ね重ね注意しておきますが、Webページ作者は、この書式を行うべきではありません。 このセクションの内容は、ユーザエージェントにとって表示処理が楽なことを示していますが、 そのURIがWebページ作者の期待通りに解釈されない危険性をはらんでいることに注意してください。

URIをUTF-8でエスケープしている場合

もし、URIのパスにのみ非ASCII文字が含まれている場合、 Webページの作者にとっては、UTF-8でエスケープしてしまうのが最も安全で有効な記述方法です。

UTF-8は他の文字コードとほとんど一致しない(しにくい)、独特のビットパターンがあります。 つまり、ユーザエージェントはアンエスケープしたバイナリ列(文字列)がUTF-8の文字列か、 そうでないかは比較的簡単、高確率で判断できます。

つまり、アンエスケープされた文字列はユーザエージェントが表示しようとした場合、 文字化けなく表示されることが期待できます。 また、エスケープ済みのURIをユーザエージェントが処理する場合、 ネットワークにURIを送信する場合はそのまま無加工で送信されるので、 Webサーバとの相性が最も良いと言えます。 つまり、Webサーバの誤動作を防ぐためにもこの記述は非常に好ましいと言えます。

しかし、URIのクエリ部分、つまり、動的なWebページへのパラメータ部分に非ASCII文字が含まれる場合、 UTF-8でエスケープしてしまってはそのWebアプリケーションが正常に動作しなくなる可能性があります。 例えば、古くから公開されているフリーのCGIプログラム等では、 クエリにEUC-JPでエスケープされた文字列が入っていることを前提としている場合があります。

つまり、この書式はUTF-8でクエリ文字列を処理しないWebアプリケーションに対するリンクの場合には使い物になりません。

URIを記述したドキュメントの文字コードでエスケープしている場合

もし、URIに含まれる非ASCII文字が、 それが記述されているドキュメントの文字コードと同じ文字コードでエスケープされている場合、 アンエスケープしたバイナリ列(文字列)をユーザエージェントはおそらくUTF-8では無いと判別可能です。 そして、HTML4.01の仕様書(上記)を参考に、ドキュメントの文字コードであるかどうか判断してみれば、 その文字列が同じ文字コードであることが確認できます。

つまり、このケースにおいてユーザエージェントは文字化けをおこさずに、 アンエスケープした文字列を表示させることが可能です。

しかし、もし非ASCII文字がURIのパスに含まれていた場合、 このURIを受け取ったWebサーバが期待通りのファイルを探し当てることができるかどうかは分かりません。 これはWebサーバ側の仕様に依存します。

しかし、UTF-8をクエリとして扱えない、レガシーなWebアプリケーションに対して、 クエリ部分をエスケープして記述するにはこの方法がユーザエージェントにとっても、 Webアプリケーションにとってもベストな選択肢であると言えます。

URIをそれ以外の文字コードでエスケープしている場合

このケースにおいて、ユーザエージェントはアンエスケープしたバイナリ列を、 ユーザに読みやすい(つまり、適切な文字コードで)表示することは不可能です。 つまり、読みやすさよりも正確性を優先させ、エスケープしたままのURIを表示することがベストと言えます。

しかし、ここには大きな問題があります。 URIに含まれる文字列の文字数なんて、ほんの短い文字列です。 つまり、UTF-8ではない、と判別できたとしても、ドキュメントの文字コードと同じかどうか判別できない、 という場合が実は大半なのです。この場合、ユーザエージェントは文字化けしたURIを表示してしまう可能性があります。 これは、ユーザエージェントのユーザビリティ向上のための処理が皮肉にも、 最もよくない処理結果になってしまうという最悪なパターンです。

Webページの作者がこの問題を回避しようと考えた場合、とれる手段はふたつあります。

一つめは、URIの文字コードに、それを記述している文書の文字コードを揃えてしまうことです。 つまり、EUC-JPの文字列を含むURIを記述する場合、EUC-JPでドキュメント自体を保存する必要があります。

二つめは、ドキュメントの文字コードをUTF-8にしてしまうことです。 前述のように、バイナリ列がUTF-8の文字列であるか否かはほぼ確実に判定することができます。 つまり、UTF-8のドキュメント内の、非UTF-8なエスケープ済みのURIは文字化けを起こさず、 エスケープされたまま表示されることが期待できます。

もちろん、これらの対処は本末転倒であることは言うまでもありません。 ユーザエージェントの都合にWebページ作者があわせる、というのも理にかなっていません。 また、何よりも、Shift_JISでエスケープしたURIと、 EUC-JPでエスケープしたURIの両方がひとつのページにある場合、一つめの解決法は使えません。

Webページ作者がユーザビリティの向上のために、URIの文字化け表示をなんとしても回避したい、 というのであればUTF-8でドキュメントを作成するという手段が有効であるとは言うことができます。

URIに複数の文字コードが混在する場合

前述のように、パスに非ASCII文字を使いたい場合、UTF-8でエスケープするのがベストです。 しかし、レガシーなWebアプリケーションがそのようなパスにある場合、 クエリ部分は、Webアプリケーションの要求する文字コードでエスケープする必要があります。

つまり、これらの要求を満たすには、パスをUTF-8でエスケープし、 クエリをそれ以外の文字コードでエスケープする必要があります。

これはユーザエージェントにとっては最も処理しにくいパターンです。 しかし、クエリ部分はWeb標準仕様に関係なく、Webアプリケーションの仕様に依存する部分ですので、 ユーザエージェントは分割して処理すべきです。そうすれば、これまで説明してきたとおり、 非UTF-8な部分がドキュメントの文字コードと一致するならユーザエージェントは表示問題を解決することは可能です。

もっとも良い解決策

パスには非ASCII文字を使わない、というのが慣例になっていますので、 静的なサイトであれば、これが最も単純な解決策になりえます。

しかし、動的なWebアプリケーションがある場合、クエリに関してはどうしようもありません。 また、Apacheのrewriteモジュールを使うことで、クエリをパスのようにしてしまうケースも存在します。 こうなってくると、非ASCII文字を使わなければ良い、というのは解決策にはなりません。

となると、最も良い解決策は、全てのドキュメントをUTF-8で作成し、 WebアプリケーションもクエリにUTF-8を利用可能なものにする。 つまり、URIは常にUTF-8でエスケープして記述する、ということです。 (それでも外部のリソースにリンクする場合等、どうしようも無い場合もあります。)

現在のWWWにおいては、各種仕様がUTF-8を念頭に置いていたり、 関連するアプリケーション(ユーザエージェント等)が内部処理をUTF-8または、 UTF-16で行うことが多いため、UTF-8が事実上、 標準の文字コードと言っても良い状況です。 ですから、ある意味当然の結論と言えます。

Unicodeに関してはさまざまな問題が提起されており、完璧な、理想的な文字コードとは言えないかもしれません。 しかし、現状、各種OSがUnicodeをベースに作成されていて、それらによってインターネットが形成されている以上、 結局Unicodeの懸案をWWWの利用者側で解決することは困難です。

テストケースと、各ユーザエージェントの対応状況

各ユーザエージェントの実装状況を確認するためにテストケースでテストしてみました。 以下はその結果です。

各ユーザエージェントでのテスト結果
ユーザエージェント 直接非ASCII文字を記述 UTF-8でエスケープ ドキュメントの文字コードでエスケープ 関係ない文字コードでエスケープ クエリをドキュメントの文字コードで、それ以外をUTF-8でエスケープ
ホストパスクエリ ホストパスクエリ ホストパスクエリ ホストパスクエリ ホストパスクエリ
Internet Explorer 6.0 文字列文字列文字列 エスケープエスケープエスケープ エスケープエスケープエスケープ エスケープエスケープエスケープ エスケープエスケープエスケープ
Firefox 1.5 文字化け文字列文字列 文字列文字列文字列 文字列文字列文字列 文字化け文字化け文字化け 文字化け文字化け文字列
Opera 8.51 文字列文字列ドキュメントの文字コードでエスケープ 文字列文字列エスケープ 不正なURLとして表示されず 不正なURLとして表示されず 文字列文字列エスケープ
Opera 8.51(IDN無しの場合) -文字列ドキュメントの文字コードでエスケープ -文字列エスケープ -エスケープエスケープ -エスケープエスケープ -文字列エスケープ

これらの結果から、Operaの結果がなかなかに優秀と言えます。

IDNは本来、互換性を考えるならpunyコードで記述されるべきですが、 仕様的にはエスケープされても良いことになっています。(RFC 3986 3.2.2参照) UTF-8以外でIDNがエスケープされている場合、Operaは不正なURIとして処理しますが、 これは仕様の範囲外の判断であるものの、セキュリティ上、妥当な判断だと思います。 前述の通り、UTF-8以外の文字コードは判別しにくいので、下手に扱うと「なりすまし」の危険性が高まるからです。

Firefoxは、そもそも、なんとかして文字列を表示しようとしすぎている感じがぬぐえません。 そのため、文字化けという副作用が多発しています。 また、IDNのエスケープ表記には対応できていません(Bug-org 309671)。 この辺は以前から指摘されていることで、2.0もしくは3.0にむけて修正されていくと思われます。

これらに対し、IE6は一切手を加えて表示しようとは試みていないことが分かります。 これは、ユーザにとって不便ではあるものの、誤った情報を提供しないという点においては、 ユーザエージェントとして無難な処理だと言えます。

RFC 3986では、URIを生成するアプリケーション等に対する制限は細かく記述されているものの、 実際に、エスケープされたIDNを処理する場合、 どの文字コードの文字列として扱うべきかという重要な点が明記されていません。 しかし、文意からUTF-8以外を用いるべきでないと意図しているように見えます。 参考のために、原典のRFC 3986と、その和訳文書を引用しておきます。

   The reg-name syntax allows percent-encoded octets in order to
   represent non-ASCII registered names in a uniform way that is
   independent of the underlying name resolution technology.  Non-ASCII
   characters must first be encoded according to UTF-8 [STD63], and then
   each octet of the corresponding UTF-8 sequence must be percent-
   encoded to be represented as URI characters.  URI producing
   applications must not use percent-encoding in host unless it is used
   to represent a UTF-8 character sequence.  When a non-ASCII registered
   name represents an internationalized domain name intended for
   resolution via the DNS, the name must be transformed to the IDNA
   encoding [RFC3490] prior to name lookup.  URI producers should
   provide these registered names in the IDNA encoding, rather than a
   percent-encoding, if they wish to maximize interoperability with
   legacy URI resolvers.

reg-name 構文は、基本的な名前解決技術に依存しないような単一の方法において非 ASCII 登録名を表すためにパーセントエンコードされるオクテットを認める。 UTF-8 [STD63] によれば、最初に非 ASCII 文字をエンコードし、その後に URI 文字として表すために UTF-8 文字列に対応する各オクテットをパーセントエンコードしなければならない。 URI 生成アプリケーションは、UTF-8 文字列を表すために使用するのでなければ、ホストにおいてパーセントエンコーディングを使用してはならない。 非 ASCII 登録名が DNS を通じての解決が意図された国際化ドメイン名を表す時、その名前は名前ルックアップの前に IDNA エンコーディング [RFC3490] へと変形されなければならない。 ければなりません。 URI の生成を行うものは、古い URI リゾルバとの相互運用性を最大にしたいならば、それらの登録名をパーセントエンコーディングよりも、IDNA エンコーディングにて提供すべきである。

参考

Masayuki Nakano(問い合わせ先)