4.8. 文字のエンコード

4.8.1. 文字のエンコードとは

長年に渡って、米国では ASCII 文字セットを使って文字のやり取りをしてきました。 米国のシステムは基本的に ASCII をサポートしているので、簡単に英語の文字で やり取りできます。 残念なことに、その他言語の文字の大半を扱うには ASCII ではまったく力不足です。 これまでずっと、さまざまな国でいろいろなテクニックを使って、さまざまな言語 で文字をやり取りしてきました。このことが、ますます相互につながりを持ちつつ ある世界で、データをやり取りすることを困難にしています。

つい最近、ISO は ISO 10646 を整備し、「Universal Mulitple-Octet Coded Character Set(UCS)」としました。 UCS は全世界の文字それぞれに対して、31 ビットの値を定義した符号化文字集合です。 UCS のはじめから 65536 文字(16 ビットに当たります)は、「Basic Multilingual Plane(BMP)」とし、今日使用されている言語をほぼカバーすることを目的としています。 Unicode コンソシアムは Unicode 規格を作成しました。これは UCS に焦点を当て、 追加でいくつか規約を設け、共同で運用できるようにしています。 もともと Unicode と ISO 10646 は競いあって制定を進めてきましたが、ありがたい ことに共同して作業をする必要があることを理解し、今ではお互いに連携しています。

多言語を扱う新規のソフトウェアを書くなら、ISO 10646 や Unicode を基本として 多言語を扱うようにしてください。 しかし、さまざまな(言語固有の)文字集合で書かれた古いドキュメントを処理する 必要があるかもしれませんので、信頼できないユーザが他のドキュメントの文字集合 をコントロールできないことを必ず確認してください。 (ドキュメントの変換処理に影響が大きいからです)。

4.8.2. UTF-8 とは

ソフトウェアの大半は、16 ビットや 32 ビットの文字を扱うように設計しておらず、 8 ビット以上が必要な多言語文字集合を作成してもいません。 そのため UTF-8 という特別なフォーマットが開発され、既存のプログラムや ライブラリが、多言語を表現できる文字列を容易に扱えるような形式にエンコード することとなりました。 UTF-8 は IETF RFC 2279 で定義してありますので、よくまとめられた規格が自由 に読めて利用ができるのは幸いなことです。 UTF-8 は可変長でエンコードします。0 から 0x7f(127)の文字は 1 バイトでその ままエンコードしますが、それより大きな値の文字は 2 から 6 バイトの情報として エンコードします(値によってバイト数がかわります)。 エンコードは、下記の属性に適合するように特別に設計してあります(これは RFC もしくは Linux に含まれている utf-8 の man からの情報です)。

要するに UTF-8 の変換フォーマットは、多言語のテキスト情報をやりとりするのに 秀でており、世界中のあらゆる言語をサポートできます。その上なお、US ASCII ファイルと下位互換性があると同時に、他の優れた属性も持っています。 いろいろな目的に採用することをお薦めします。「テキスト」ファイルにデータを 保存する場合にはなおさら。

4.8.3. UTF-8 のセキュリティ上の課題

UTF-8 に言及する訳は、バイト列に不正な UTF-8 があると、それがセキュリティ ホールになるかもしれないからです。 UTF-8 は、「最短」エンコードの利用を想定していますので、そのままデコード すると、必要以上に長い文字列をエンコードしたものを受けてしまうかもしれない からです。 実際、初期の規格では「最短」でないエンコードを認めていました。 ここで問題となるのは、複数の方法で危険な入力が行われる可能性があり、その ことによって、危険な入力に対するセキュリティ処理が無になるかもしれないと いうことです。 RFC ではその問題を下記のように記載しています。

UTF-8 の実装では、不正な UTF-8 文字列をどのように対処するか、という セキュリティ上の観点を考慮する必要があります。環境によっては、攻撃者が 無防備な UTF-8 パーサに UTF-8 の文法では認められていないオクテット文字列 を送りこみ、悪用してしまうことも考えられます。

この攻撃は、入力に対してセキュリティに重点をおいた正当性チェックを行う パーサに対して、実に巧妙に実行されます。UTF-8 でエンコードしてあるものの、 文字として不正なオクテット列として解釈されてしまう入力がそれに当たります。 たとえば、パーサは 00 という単独のオクテット文字がエンコードされた場合には NULL 文字を禁止しているかもしれません。しかし不正であるオクテット文字 2 文字である C0 80(必要以上に長い)は許しており、それを NUL 文字(00)として 処理しています。その他の例としては、オクテット文字列である 2F 2E 2E 2F ("/../")は禁止していますが、不正である 2F C0 AE 2E 2F は許してしまっています。

この件についてのさらなる論議は Markus Kuhn 氏のサイト http://www.cl.cam.ac.uk/~mgk25/unicode.html にある UTF-8 and Unicode FAQ for Unix/Linux で読めます。

4.8.4. UTF-8 の正しい値

つまり、UTF-8 を入力として受け付ける場合は、その入力が正しい UTF-8 なのかを チェックする必要があります。 ここで挙げる一覧は、正しい UTF-8 文字列すべてです。文字がこのテーブルに マッチしなければ、正しいとは言えません。 下記のテーブルで 1 番目のカラムは UTF-8 にエンコードする各種文字コードです。 2 番目は文字がどのようにバイナリにエンコードするかを示しています。「x」は データがあること(0 か 1)を示しますが、最短エンコードでない場合には認める べきでないケースもあります。 最後は、それぞれのバイトが取りうる正しい値(16 進表示)です。 したがって、個々の文字が右側のカラムのパタンのどれに当てはまるのか、プログラム でチェックする必要があります。 「-」は正しい値の範囲(両端を含む)を表わしています。 もちろん、文字列が正しい UTF-8 の文字列であると言うことだけで、受け入れて良い とは言えませんが(その他のチェックも必要)、普通、他のチェックをする前に UTF-8 の正当性をチェックする必要があります。


Table 4-1. Legal UTF-8 Sequences

UCS Code (Hex)Binary UTF-8 FormatLegal UTF-8 Values (Hex)
00-7F0xxxxxxx00-7F
80-7FF110xxxxx 10xxxxxxC2-DF 80-BF
800-FFF1110xxxx 10xxxxxx 10xxxxxxE0 A0*-BF 80-BF
1000-FFFF1110xxxx 10xxxxxx 10xxxxxxE1-EF 80-BF 80-BF
10000-3FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxxF0 90*-BF 80-BF 80-BF
40000-FFFFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxxF1-F3 80-BF 80-BF 80-BF
40000-FFFFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxxF1-F3 80-BF 80-BF 80-BF
100000-10FFFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxxF4 80-8F* 80-BF 80-BF
200000-3FFFFFF111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxxtoo large; see below
04000000-7FFFFFFF1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxxtoo large; see below

先に触れたように、文字集合には ISO 10646、Unicode という 2 つの規格が ありますが、文字の割り当てに関しては同期を取っています。 現状、ISO/IEC 10646-1:2000 と IETF RFC における UTF-8 の定義では、5、6 バイト文字列のエンコードもサポートしており、文字を Uniforum の Unicode の範囲外にエンコードしています。しかしそのような値は、Unicode 文字としては サポートされてこなかったので、ISO 10646 の将来のバージョンでも同様の制約 を受けると思われます。 つまり、5、6 バイトの UTF-8 のエンコードが正しいケースはほとんど無く、 通常は拒否しなければいけません(特別な目的が無い限り)。

正しい値の範囲を特定するのは困難です。そして実際このドキュメントの初期の版 では、間違った項目がいくつか記載してありました(長すぎる文字を許してしまって いるケースがありました)。 言語開発者は、ライブラリに正しい UTF-8 の値をチェックする機能を入れる 必要があります。チェックを正しく行うのはとても困難なので。

場合によって、16 進の C0 80 に対して、それほどシビアにしたくない(もしくは 内部で何とかしたい)ケースがあるかもしれない、という点を示しておきます。 これは長すぎる文字列で、許可してしまうと ASCII の NUL(NIL)に相当することに なります。C と C++ では NIL 文字を通常の文字列に入れてしまうとやっかいな ことになりますので、データストリームの一部として NIL を表したい時に、この並び を用いるケースがあります。Java ではこの操作を正式に記載しています。 データ処理を行う時に、内部的には C0 80 を好きに扱ってください。ただし、厳密に 言うと、そのデータを保存する前に 00 に変換し直す必要があります。 必要性にもよりますが、「シビアにならずに」、C0 80 を UTF-8 のデータ ストリームとして認めてもよいかもしれません。 セキュリティに影響がでないなら、運用を助けるという観点で許可するのはよい案 だと思います。

この対処は微妙です。 Unicode フォーラムで開発した C による変換ルーチンを調査したいなら、 ftp://ftp.unicode.org/Public/PROGRAMS/CVTUTF/ConvertUTF.c を 利用してみてはどうでしょうか。 このルーチンがオープンソースかどうかはっきりしませんので(ライセンスを読んでも、 修正可能かどうかわかりません)、その点は注意してください。

4.8.5. UTF-8 関連について

このセクションでは UTF-8 を論じます。UCS で多バイトをエンコードするのに 最も一般的で、各種の国際化テキストが抱える問題を簡単に扱えるからです。 しかし、それだけがエンコードでないのも事実です。他のエンコードとして UTF-16 や UTF-7 のようなエンコードがあり、同様な問題を抱えていますので、 同じような理由で検証しなければいけません。

もう 1 つの問題として、複数の表現法で表せるものが ISO 10646 や Unicode に ある点です。 たとえば、アクセント文字の中には 1 文字(アクセント付き)で表現できるもの がありますが、文字の組み合わせ(たとえば、ベースになる文字にアクセントをのせる) で表現できるものもあります。 この 2 つの形式は、同一であるかもしれません。 幅がないスペースを挿入した結果、異なるものが見た目同じように見えることもあり ます。 そのような隠れたテキストが存在する状況において、プログラムに影響が出る点に 注意を払ってください。 これは一筋縄では行かない問題です。プログラムは、特定の文字列をどのように表示 するのかを完全に掌握しているクライアントに対して、そのようなきつい制約を かけていない場合が大半です(クライアントのフォントや表示特性、ロカール等に 依存しているので)。