mbstring/libmbfl 正しいUTF-8チェックの強化

非最短形式UTF-8などのmbstringでUTF-8のチェックが甘いという指摘がでてからかなり立ちますが、PHP 5.4に向けて、対策を考えてみました。

libmbfl 1.3.1 からは、UTF-8の変換や検出時に行われる文字コード範囲検出において整形式であることを確認するチェックを導入します。上記の不正なバイト列は無効な文字として判定され、指定した処理が行われます。libmbfl 1.3.1 は PHP 5.5dev, PHP 5.4beta1 に適用予定です。

Unicoe/UTF-8の定義と背景

Unicodeの規格(最新は昨年10月にリリースされたUnicode 6.0)では、Unicodeは21bit空間(0-0x10FFFF)で定義され、約111万のコードポイントを有しますが、Unicode 6.0では約11万個について文字が登録されています。当初、16bit(UCS-2)で文字集合が定義されたUnicodeは当然ながら足りなくなり、31bitのUCS-4まで拡張されたのですが、21bitで落ち着いたようです。Unicodeは、業界団体であるUnicodeコンソーシアムと規格標準化組織であるISO/IEC 10646の共同作業で規格化が行われていますが、前者の方がより実装に近いより詳細な規格を定義しています。

UTF-8は、ASCIIの上位互換(西欧圏の人にやさしい)、先頭バイトの判別が容易などの特徴から使い勝手が良かったこともあり、インターネット環境で広く普及しています。UTF-8は、当初、UCS-4のコード空間をサポートするために最長6バイトの定義でした。当初のUTF-8の定義にはあいまいなところがあり、非最短形式のUTF-8というものが定義できてしまうため、RFC2279で指摘されているようにNULLバイト攻撃やパストラバーサル攻撃を受ける可能性が指摘されてきました。

非最短形式UTF-8のリスク

非最短形式UTF-8とは、例えば、'<' (0x3C)が 0xC0 0xBC、0xE0 0x80 0xBC、0xF0 0x80 0x80 0xBCとしても符号化できてしまうということです。'<'ならそう問題も起きないかもしれませんが、NULL文字(0x00)やドット '.' (0x2E)などは文字列の終端やパスに使用されているので、問題を発生する可能性があります。特にNULL文字などやドットに関するセキュリティチェックは、通常最短形式で行うため、このチェックをすり抜けられる可能性があることで重大な問題を引き起こす可能性があります(実際にIISなどはこのバグをつかれて有名なセキュリティ上の問題(Nimdaワーム)を発生しました)。非最短形式を解釈しないパーサと解釈するパーサが混在する場合に問題は深刻になります。

(なお、後述するように)非最短形式UTF-8についてはPHP 5.3においても一定の対策が行われています。

整形式(Well-Formed) UTF-8


整形式(Well-Formed) UTF-8は、http://www.unicode.org/versions/Unicode6.0.0/ch03.pdf#G7404
で以下のように定義されています。

Code Points First Byte Second Byte Third Byte Fourth Byte
U+0000..U+007F 00..7F
U+0080..U+07FF C2..DF 80..BF
U+0800..U+0FFF E0 A0*..BF 80..BF
U+1000..U+CFFF E1..EC 80..BF 80..BF
U+D000..U+D7FF ED 80..9F* 80..BF
U+E000..U+FFFF EE..EF 80..BF 80..BF
U+10000..U+3FFFF F0 90*..BF 80..BF 80..BF
U+40000..U+FFFFF F1..F3 80..BF 80..BF 80..BF
U+100000..U+10FFFF F4 80..8F* 80..BF 80..BF

2バイト定義の1バイト目がC2から始まること、および *を付けたところがポイントになります。
C0 AF, E0 9F 80などはこの範囲にないので、ill-formedとなります。

以下の例は上記のUnicode 6.0のドキュメントにある例で、不正なバイトを含んでいます。

mb_substitute_character(0xFFFD);
$s = "\x41\xc0\xaf\x41\xf4\x80\x80\x41";
echo mb_convert_encoding($s,"UTF-16","UTF-8");

出力は、以下の通り、Unicodeで推奨されるエラー時の変換結果となります(2011/9/7修正)。

U+0041 U+FFFD U+FFFD U+0041 U+FFFD U+0041

なお、不正なバイト列を検出した時の処理をどうするかも課題です。
現状では、何も返さないことや不正なバイトを返すことは新たなリスクを生み出すため、BAD+XXという形式(longをsubstitute_charに設定した場合)を返しています。また、代替文字にU+FFFDを設定した場合は、Unicode規格の「Constraints on Conversion Processes」にある推奨されるエラー処理結果となります。

PHPバージョン間の違い(2011/9/7追記)

PHP 5.2, 5.3 との違いについてわかりにくいので追記します。 PHP 5.2までは不正なUTF-8に関するチェックは甘かったのですが、PHP 5.3で一定の対策がされるようになっています。これは、例えば 3バイトのUTF-8 の場合は、U+0800〜U+FFFFという範囲にあることを確認するもので、非最短形式UTF-8については無視される(何も出力されない)ようになっています。

ただし、何も出力しないことでその前後のバイトが特別な意味を持ち、新たなリスクを生み出す可能性も指摘されており、基本的には不正な文字をユーザが指定した代替文字に置き換える手段を提供するべきです。また、不正なバイト列を判別し、代替文字を出力するタイミングもUnicodeのドキュメント記載の通り、不正なバイト列と判明した時点で行うことが望ましいと言えます。

以下の例は、Unicode 6.0のドキュメントに記載された不正なバイト列を含んでいます。

mb_substitute_character(0xFFFD);
$s = "\x41\xe0\x9f\x80\x41";
echo mb_convert_encoding($s,"UTF-16","UTF-8");

各バージョンの最新版(開発版)での結果は以下となります。

PHP 5.2.18dev: U+0041 U+07C0 U+0041
PHP 5.3.8dev: U+0041 U+0041
PHP 5.4dev U+0041 U+FFFD U+FFFD U+0041

PHP 5.2では、不正なバイト列を出力してしまっています。PHP 5.3は不正な文字列を判別していますが、その部分には何も出力されません。PHP 5.4では不正なバイト列 E0 9F および 80 に対応した代替文字(U+FFFD)が出力されます。

TODO

今回の処理に関しては、実運用の前に十分なテストを行う必要があります。

想定されるリスクには以下のようなものがあります。

  • UTF-8のチェックが厳しくなることで、下位互換性が失われる
  • UTF-8のチェックが厳しくなることで、通信エラー等に対する耐性が大幅に低下する
  • 厳しいチェックを行うパーサと緩いパーサが混在することによるリスク
  • 実装のバグにより生じる問題(セキュリティリスク等)

** libmlfl(mbstring)への絵文字のサポート追加

Unicode 6.0に携帯電話の絵文字が正式に組み込まれ、Mac OS Lion等でもサポートされるなど、使う機会も増えてきたということで、mbstringに携帯電話のShift_JIS文字コードのサポートを組み込んでみました。

現時点で、githubのlibmbfl開発コード、PHPのHEADおよび5.4ブランチにコミットされていますので、試用することができます。

現時点でサポートされるのは、以下の機能です。

前者がメインの機能ですが、現時点で携帯会社はUTF-8のPUAとの相互変換に基づく運用していますので、レガシー機能のサポートも行っています。

Unicode 6.0を介した絵文字コードの相互変換

複数の携帯電話間の絵文字を含むShift_JISコードの文字コード変換を行います。
Unicodeコンソーシアムが公開するDoCoMoKDDISoftBankShift_JISコード絵文字とUnicode 6.0との相互変換表に基づき実装されています。
現時点で、Unicode 6.0を直接サポートするシステムは、先日リリースされたLionくらいですが、今後、サポートするOSも増えるものと予想しています。

Unicode 6.0との相互変換が可能とするために、以下の3つの文字コードを追加しています。

SoftBank絵文字では、Webコードを含むShift_JISも解釈します。

DoCoMoの絵文字からKDDIの絵文字に変換する例を以下に示します。

echo mb_convert_encoding("\xf8\x9f", "SJIS-Mobile#KDDI", "SJIS-Mobile#DOCOMO");

なお、現時点で対応する絵文字がない場合のフォールバックはサポートされません。ただし、代替文字への置換を行うPHPスクリプトを作成することは比較的容易であると思います。

絵文字は、合字(国旗など)も含めてサポートされますが、Unicode 6.0に含まれない一部の文字(iモードのロゴなど)はサポートされません。

Unicode PUA を介した絵文字の変換

Shift_JISUTF-8への相互は現時点でベンダ毎に異なるUnicode PUAとの相互変換としてサポートされています。以下のコードによりShift_JISUnicode PUAの変換が可能です。

(2011.8.13追記: なお、PUAを介した変換はUnicodeとのマッピングを2種類提供するということで、混乱を発生することになる可能性があります。現在、UTF-8+PUAとUnicode 6.0の間のマッピングを提供し、Shift_JIS からUnicode PUAへのマッピングを提供しないという案とのトレードオフをしています。以下の文字エンコーディングについては、PHP 5.4.0に採用されない可能性がありますので、ご注意ください。)

最後の1つ(SJIS-Mobile#KDDI-PUA-B)は、俗称KDDI裏コードと呼ばれるPUAのマッピングをサポートします。これは、PUAへのマッピングが一部SoftBankの領域と干渉するためです。KDDIの携帯電話は、UTF-8のページで送信してくるデータについてこのコードを使用しているとのことです。

現時点で、各ベンダの規定するPUAを絵文字領域として使用するUTF-8からUnicode 6.0への絵文字領域への変換を直接行う機能はサポートされません。

以下にKDDI絵文字を含む(KDDIが規定するPUA領域マッピングを含む)UTF-8からUnicode 6.0に基づくUTF-8に変換する例を示します。

$sjis = mb_convert_encoding($s, "UTF-8", "SJIS-Mobile#KDDI-PUA", "UTF-8");
echo mb_convert_encoding($sjis, "UTF-8", "SJIS-Mobile#KDDI");

注意

合字の処理などを含むため、現時点でのUnicode 6.0のサポートは実験的なものです。
(国旗のUnicode 入りは政治的にもめたらしいけど、もう少しなんとかならかったのかな。。。)
PHP 5.4.0の正式リリースまでに十分なテストを行う必要があります。

トップの現場の判断

 本日の朝日の記事にTDLの人材育成に関する書籍「ディズニーの教え方」の紹介記事が載っていた。9割がバイトでも高い顧客満足度を得る最高のスタッフが育つ理由に関するもの。スタッフに「自由に考え行動できる最良を与える」というのが重要だそうだ。マニュアルに忠実な人材を育てるのではなく、会社のミッションを示し、自主判断を重視するのだという。ミッションを明確に示し、行動指針をそれに基づき優先順位を付けて4つ(「安全」、「礼儀正しさ」、「ショー」。「効率」)示している。
 先日の地震の際にスタッフがとった行動は、アトラクションの外に客を誘導するところまではマニュアル通りだったが、その後、店舗の商品である菓子類を配り始めたり、通常見せてはいけないバックヤードを退避経路に使ったのは現場の判断だったそうだ。行動指針の優先順位の最上位が「安全」であることを理解していればマニュアルがなくても行動できるのだが、これは言うほどやさしいことではない。行動指針自体は多くの組織の中で示されていると思うが、想定外の事象に対応する実際の行動に結びつけられるだろうか?
 マニュアル通りに行動することを意図した教育だった場合には、マニュアルにないことが起きた瞬間に対応できなくなる。そして誰もが指示待ちになり、時間だけがむなしく過ぎていくことになる。震災の際の東京電力や国などの対応と比較するとわかりやすい。
 危機管理は経営のテーマにしばしばなるが、とても難しいテーマだ。皆がついていくわかりやすい理念と明確な行動指針を示せるかどうか、そしてそれを表面上ではなく本当の意味で浸透させられるかが鍵になるのだろう。

新聞と私

 思い起こせば新聞を読み始めたのは小学生の3、4年くらいからだろうか。ジャイアンツが好きな父の選択で読売新聞を読んでいた。社会人になってからは、会社の寮の食堂で朝食を食べながら朝日新聞をのんびり読むのが日課だった。残念なことにエンジニアの同期では新聞をじっくり読む人は少ないように見えた。新聞を読む私のペースは朝の慌しい雰囲気の中で極端に遅く、同じテーブルに座る人が3回転くらいしてからおもむろに出勤していた。
 家を持った今も朝早めに起きて新聞を読んでから出勤するペースはそれほど変わらない。妻のペースを乱さないように気を遣う必要がある程度の違いだ。
 新聞の良いところは、自分が興味がある内容については記者の視点でアレンジされた情報を入手でき、また、なにより普段の自分が興味を持たないであろう情報に出会える機会を持てることだと思う。
 若手のエンジニアや学生に新聞を読まない理由を聞くと、ネットやテレビで十分情報が入手できるからと応える。また、社会のニュースに関心が低い傾向は私の世代にも共通している。ネットは確かに素晴らしい。多くの情報を瞬時に得ることができる。ただし、自分の興味がある情報を見出しだけでひろっていくだけでは、自分の感性や思考は広がっていかないのではと思う。
 社会のあらゆることは、単純に白黒がつかないことが多く、それを理解して判断していくことは、エンジニアに限らず何より重要だと思うのだが、わかりやすくアレンジされたテレビ番組のせいなのか、そもそも異なる意見があることについていけない人が多いように思う。原発の問題にしても、有事になると、ヒステリックな一方的な批判が起きたりする。
 朝日にしても読売にしても、当然、記者の視点が入るので記事にはバイアスがかかっているのだが、例えば朝日の特集では原発に賛成と反対の意見を同時に載せることで多面性を保つ努力をしている。賛成派の元議員が「低レベルの放射線が体に良い」という意見をこの状況で主張するのには少し驚いたが、エネルギーの問題が単純ではないことは世界中の人が知っている話だ。今日の朝日の記事でも脱原発に動き始めたドイツがフランスの原発で作った電気に依存していることなどが書いてある。そして、新聞の良いところは、ある事件の後を追ってくれるような記事があることだ。テレビのニュースは一過性でインパクトがある良い画像がとれないと紹介されない。しかし、劇的な事件や事故の被害者は画像にするにはインパクトがあまりにないその後の人生が続くことになる。新聞はたまにそのような記事をとりあげることがあり、当事者が考えることを知ることができる貴重な機会になる。
 先日、遅ればせながら朝日新聞のデジタル版が始まった。(私はまだ持っていないが、)スマートフォンで読むには便利だろう。新聞配達の人が毎朝届けてくれる紙メディアもあと10年もすれば主流ではなくなるかもしれない。紙メディアが衰退するのは時代の流れで、電子メディアには検索ができ、動画も使えるという重要な利点があるので、肯定的にとらえているが、多面性があり、物事を深堀りするメディアはぜひ残ってほしいと思う。ビジネスモデルも変わるのかもしれないけれど、今の新聞購読料程度だったら払いますよ (^_^)