PHP 5.4におけるhtmlspecialchars()の問題

PHP 5.4 beta 2が出ているところですが、ソースコード(html.c)を見ていて、XSS対策で使用される htmlspecialchars()の文字コード指定がPHP 5.3とは異なる動作をすることに気が付きました。

内部文字コード(mbstring.internal_encoding)が使用される以下のようなシチュエーションです。

<?php // test.php
echo htmlspecialchars("foo",ENT_QUOTES,"");

mbstring.internal_encoding="SJIS-win"と指定することにします。(推奨はできませんが、)外字対応(絵文字対応)でCP932を使用したい場合を想定しています。

  • PHP 5.3で実行
php53/sapi/cli/php -d mbstring.internal_encoding="SJIS-win" test.php

出力は、"foo"となります。 PHP 5.3では、"SJIS-win"はShift_JISと認識されます。

  • PHP 5.4で実行
php54/sapi/cli/php -d mbstring.internal_encoding="SJIS-win" test.php

以下のように警告が発生します。

Warning: htmlspecialchars(): charset `SJIS-win' not supported, assuming utf-8 in /home/rui/work/php/src/test.php on line 2
foo

この場合、SJIS-winが有効な文字コードとして認識されず、htmlspecialchars用の文字コードとしてデフォルトの文字コードUTF-8)が使用されてしまいます。上記の警告を無視して対策を講じない場合、XSS等に関するリスクを発生する可能性があります。

対策としては、やはりhtmlspecialchars()の第3引数に明示的に文字コードを指定するのが良いでしょう。日本語の文字コードとして有効なのは、Shift_JIS系では"Shift_JIS"、"SJIS"、"932"、EUC-JP系では"EUCJP","EUC-JP"です。

<?php
echo htmlspecialchars("foo",ENT_QUOTES,"SJIS");

なお、htmlspecialchars("foo",ENT_QUOTES,mb_internal_encoding()) のようなコードも存在するようですが、上記の設定ではSJIS-winとなってしまいデフォルト値(UTF-8)が使用されるため、使用できません。また、そもそも0x5c問題などを誘発しやすいSJIS系の文字コードを内部文字コードに使用するのは避けるべきと思います。

PHP 5.3系では文字コード指定を省略した場合、デフォルト値としてISO-8859-1が使用されていましたが、PHP 5.4系では UTF-8が使用されます。UTF-8を使用する場合は便利になりますが、しばらくはバージョンが混在して使用されるケースもありますので、やはり、明示的に文字コードを指定する方が良いでしょう。

(P.S.)
SJIS-win、CP932、eucJP-winについてPHP 5.3と動作が同じとなるようにパッチをコミットしておきました。

Google+ API をPHPから使ってみる

Google+ APIが一般公開された(Getting Started on the Google+ API - Google+ Developers Blog)のを受けて、PHPでアクセスしてみました。 シンプルなOAuth2のBEARERトークンなのでcurlエクステンションで書いてみても良いのですが、以前作った簡易OAuth2クラスLightOAuth2(Google Code Archive - Long-term storage for Google Code Project Hosting.)を使ってみました。

アクティビティを取得するコードのサンプルを以下に示します。Google APIコンソールでクライアントID、シークレット、コールバックURLを登録して実行します。
デフォルトでは、example_googleplus.phpがファイル名ですが、CALLBACKとあわせて適当に変更してください。

まだ、Google+ APIの種類は少ないようですが、整備が進むといろいろできそうです。

<?php
require_once('LightOAuth2.php');
define('CLIENT_ID','<your client id>');
define('CLIENT_SECRET','<your client secret>');
define('SCOPE','https://www.googleapis.com/auth/plus.me');
$entry = array('authorize'=>'https://accounts.google.com/o/oauth2/auth',
	       'access_token'=>'https://accounts.google.com/o/oauth2/token');

// path for the application script (it should be registered on Google API console)
define('CALLBACK','http://www.example.com/example_googleplus.php');

$oauth = new LightOAuth2(CLIENT_ID, CLIENT_SECRET);
   
session_start();
if (!isset($_SESSION['access_token'])) {
  if (!isset($_GET['code'])) { // get authorization code
    $opts = array('scope'=>SCOPE);
    $url = $oauth->getAuthUrl($entry['authorize'], CALLBACK, $opts);
    header("Location: " . $url);
    exit();
  }
  // get access token
  $obj = $oauth->getToken($entry['access_token'], CALLBACK, $_GET['code']);
  $_SESSION['access_token'] = $obj->access_token;
}

// access to proteced resource
$oauth->setToken($_SESSION['access_token']);
$url = "https://www.googleapis.com/plus/v1/people/me/activities/public";
$response = $oauth->fetch($url);
$obj = json_decode($response);
print_r($obj);
?>

mbstring(libmbfl)で携帯絵文字サポート:Unicode 6.0に含まれない文字

絵文字サポートに関する続きです。

当初、Unicode 6.0とのマッピングが定義されない文字(iモードのロゴなど)は、使用頻度も低いと思われるため、サポートしない方針でしたが、リクエスト頂いたため、Unicode 6.0の私用面との相互変換を定義することにしました。従来より携帯電話の絵文字が利用するBMPのPUAを使用する案もありますが、ここでは、次の理由によりGoogleがemoji4unicodeで定義している私用面(FXXXX)へのマッピングを使用することにしました。

1.BMPの私用域(PUA)は他の外字と干渉する可能性が比較的高い。
2.Googleマッピングはロゴを比較的近くに集めているため、定義が容易。
3.Googleマッピングはそれなりに認知されている。

具体的な各キャリアとのマッピングを以下に示します。
このマッピング追加によりUnicode 6.0の定義も含めて全ての絵文字が使用可能となっています。

## Emoji Compatibility Symbol
# Unicode;DoComo;KDDI;Softbank
FE4C5;;;FBAA
FE82D;F986;F748;
FE83C;;F7A3;
FEB89;;F7D2;
FEE10;F975;;
FEE11;F976;;
FEE12;F978;;
FEE13;F979;;
FEE14;F9B1;;
FEE15;F9B2;;
FEE16;F94A;;
FEE17;F94B;;
FEE18;F94C;;
FEE19;F94D;;
FEE1A;F94E;;
FEE1B;F94F;;
FEE1C;F953;;
FEE1D;F954;;
FEE1E;F958;;
FEE1F;F959;;
FEE20;F95A;;
FEE21;F95F;;
FEE22;F960;;
FEE23;F961;;
FEE24;F962;;
FEE25;F963;;
FEE26;F964;;
FEE27;F965;;
FEE28;F966;;
FEE29;F967;;
FEE2A;F968;;
FEE2B;F969;;
FEE2C;F96A;;
FEE2D;F96B;;
FEE2E;F96C;;
FEE2F;F96D;;
FEE30;F96E;;
FEE31;F96F;;
FEE32;F970;;
FEE33;F971;;
FEE40;;F794;
FEE41;;F7CF;
FEE42;;F370;
FEE43;;F478;
FEE44;;F486;
FEE45;;F48E;
FEE46;;F48F;
FEE47;;F490;
FEE48;;F491;
FEE49;;F492;
FEE4A;;F493;
FEE70;;;FBD8
FEE71;;;FBD9
FEE72;;;FBDA
FEE73;;;FBDB
FEE74;;;FBDC
FEE75;;;FBDD
FEE76;;;FBDE
FEE77;;;F7F4
FEE78;;;F7F5
FEE79;;;F7F6
FEE7A;;;F7F7
FEE7B;;;F7F8
FEE7C;;;F7F9
FEE7D;;;F7FA

KDDI (AU)携帯電話のUTF-8絵文字の仕様

AU携帯電話の絵文字のUTF-8はよくわからない。KDDI/AUでutf-8のHTMLフォームから送られてくる絵文字コード - Bulknews::Subtech - subtechと同じテスト(Shift_JISでF641の字を表示)をしてみたが、私の携帯電話(W61P: 6.2.0.13.2) では以下の結果となった。
(参照ページの結果と同じ)

文字コード 結果
UTF-8公式Unicode NG ('?'で置換)
UTF-8非公式Unicode OK
公式Unicode数値エンティティ(&#xXXXX;) OK
非公式Unicode数値エンティティ(&#xXXXX;) NG ((火)に相当する外字を表示)

これを見る限り、公式マッピングに基づくUTF-8であるlibmbfl/mbstringの UTF-8-Mobile#KDDI は実際には使用することはできないため、UTF-8-Mobile#KDDI-BをUTF-8-Mobile#KDDIとするべきだと思う。

非公式なマッピングを前面に出すのはなんだなのだが、

- UTF-8-Mobile#KDDIUTF-8-Mobile#KDDI-A に変更(または廃止)
- UTF-8-Mobile#KDDI-B にエイリアスUTF-8-Mobile#KDDI を設定

が妥当なのかもしれない。

KDDIの仕様書にも数値エンティティに関する記述しかないので、上記のようにUTF-8でコード化することは規格外ということなのかもしれない。最近の携帯電話やスマートフォンの動作はどうなんだろう?

Shift_JIS-2004 (JIS X0213:2004) のサポートを libmbfl に追加

JIS X0213-2004 のサポート

夏休みの課題として、最後の大物の JIS X0213-2004 をlibmbfl に追加してみました。
実装面では、メモリのフットプリントを気にしてJIS X0213のテーブルをJIS X0208 に関する差分で作成することも検討しましたが、非常に見難くなりそうなので、独立したテーブルとして構成しています。いつの日か、JIS X 0208 が完全に JIS X 0213に置き換わる時が来るのではと思います。

具体的な文字エンコーディングとして、Shift_JIS-2004、EUC-JP-2004、ISO-2022-JP-2004があるようですが、まずは、Shift_JIS-2004 を実装してみました。現在、とりあえず動くものがgithub.com に入っています。EUC-JP-2004についても動くようになってきています。
ISO-2022-JP-2004はまだあまりやる気がありません。
ISO-2022-JP-2004も追加しました(8/20)。

CP932系の外字領域と干渉するため、どの程度の需要があるのかはわかりませんが、OSでもサポートされているため、だんだんと普及するのかもしれません。

使用法

現時点で、github のlibmbflオフィシャルレポジトリからソースコードを取得できます。PHPソースコードへの組み込みもconfig.m4 に mbfilter_sjis_2004.c を追加する程度でうまくいいくはずです。
(手元のテストはPHPに組み込んだ状態で行っています)

PHP 5.4系,PHP 5.5系にコミット(8/20).

mbstring/libmbflへのgb18030サポート追加

GB18030 のサポート

大国中国のオフィシャルな文字コードということで以前から気にはなっていたのですが、夏休みということでトライしてみました。
なにしろ、Unicodeのコードポイントを超える150万字をサポートする世界最大と言っても良いかもしれない文字コードです。以前、mbstring/libmbfl用にgb18030用パッチを作成された方がいらっしゃいましたが、その時のパッチは30Mバイトくらいになっており、さすがに取り込むのは困難だったような記憶があります。

Unicodeの16面をすべて含むというスケールの大きさはさておいて、実態は、GB2312、GBK、CP936といった既存の文字コードを拡張し、Unicodeとの相互マッピングを行えるようにしたものです。Unicodeの部分は算術変換で記述できるようです。

GB18030は、1バイト領域+2バイト+4バイト(Unicode)という作りで、構造的には、他のページにも書いてありますが、UTF-8の中国語版と言えるかもしれません。ただし、UTF-8と異なり、1バイト目で2,4バイトが区別できる作りになっていません。2,4バイト文字を区別するには、2バイト目を見る必要があります。これは、GBKとの互換性を保つため、1バイト目を変えられなかったからだと思いますが、結果的に文字列長を調べることを困難にしています。

libmbflへの実装ですが、ucmのようなコードページをそのまま実装してlibmbflを巨大化させることには問題があるため、すでにlibmbflでサポートされているCP936をベースに算術変換をできるだけ使って、追加データを最小化することにしました。

コードの実装

参考にしたのは、perl の Encode::HanExtra、libiconv、ICU (GB 18030 )とかです。よくわからないのは、Euro記号をCP936互換で GB+80=U+20ac とするのか、GB18030マッピングを採用するのか、0xFF はどうするのか等です。UCMファイルは、Encode::HanExtra、ICUで提供されていますが、これらは微妙に異なるようです。

また、U+1E3F,U+E7C7 のマッピングICUとlibiconvでは事なるようです。現在は、libiconvのマッピングを使用しています。

各実装を見てみると、Encode::HanExtraは、ucmを読み込んでマッピングを行っているシンプルな実装のように見えます(間違えていたらごめんなさい)。ICUは、算術符号をUnicode に対応する4バイト符号のところに使っているようです。libiconvは最もアグレッシブに算術変換を駆使しています。

ということで、libiconvを参考に実装コードを作成してみました。Unicode BMPをサポートするところは、基本的に連続しているところが多いのですが、この部分はオフセットをテーブルで読みながら算術変換することになります。UnicodeBMP以外は算術変換でできます。その他の部分は、既存のCP936の変換テーブルを拡張して対応し、残ったところは、テーブルを作成します。これで、マッピングテーブルをあまり増やさずにサポートできそうということになりました。

現状は、Encode::HanExtra付属のgb18030.ucmを使って確認していますが、よく仕様がわからない0xFF (gb18030.ucmに2種類定義されている!)以外は変換できています。

PHPへの適用

PHP 5.4に含めるかどうかはまだ決めていません。このページを中国の方が見ているとは思えませんが。。。

PHP 5.4, PHP 5.5 に適用しましたので、svn.php.netから取得することができます。

使用法

現時点で、github のlibmbflオフィシャルレポジトリからソースコードを取得できます。PHPソースコードへの組み込みもconfig.m4 に mbfilter_gb18030.c を追加する程度でうまくいいくはずです。
(手元のテストはPHPに組み込んだ状態で行っています。)

PHP 5.4.0 beta 1 が公開された時点でテストできます。

mbstring/libmbfl モバイル用UTF-8

Unicode 6.0 <-> モバイル用UTF-8の相互変換を可能としました。実際のコードをテストするには、github.com のlibmbfl のレポジトリか、svn.php.netからPHP 5.4または5.5の開発用ソースコードを取得ください。

以下、仕様等について記述します。

UTF-8-Mobileの必要性

UTF-8に関しては、現状でキャリア毎に異なるマッピングでPUA領域に絵文字が設定されています。
また、Unicode数値エンティティについてもPUA領域にマップされることが前提の仕様となっています。

前稿で、Shift_JISからUnicode PUAへのマッピングをレガシー(現状?)サポートとして作成しましたが、Unicode 6.0をベースにしたシンプルな実装を目標とすると、Shift_JISからUnicodeへのマップが2通りある(Unicode PUAとUnicode 6.0)のは混乱を招くような気がします。

Unicodeを直接使用するのは、数値エンティティの時くらいと考えると、Shift_JISからUnicode PUAへのマッピングを作るよりも、Unicode <-> UTF-8マッピングでモバイル用UTF-8を定義する方が混乱が少ない気がします。この場合、UnicodeマッピングUnicode 6.0ベースのものだけとなり、各フィルタはUnicode 6.0との相互変換を定義するというlibmbflの他のフィルタと同じ動きとなります。

UTF-8-Mobileの追加

以下のモバイル用UTF-8を定義し、Unicode 6.0との相互変換を可能とします。

- UTF-8-Mobile#DOCOMO : DoCoMo定義のUnicode PUAに基づくUTF-8
- UTF-8-Mobile#SOFTBANK: SoftBank定義のUnicode PUAに基づくUTF-8
- UTF-8-Mobile#KDDI : KDDI定義のUnicode PUAに基づくUTF-8
- UTF-8-Mobile#KDDI-B : KDDI定義のUnicode PUAに基づくUTF-8 (通称:裏KDDIコード)

UTF-8Shift_JIS間の変換はSJIS-Mobile#DOCOMO-PUA経由の時よりも以下のようにすっきり書けます。

$utf8 = mb_convert_encoding($sjis, "UTF-8-Mobile#DoCoMo", "SJIS-Mobile#DoCoMo");

なお、KDDIについては、UTF-8のページではフォーム送信時に裏KDDIコードを使用するとのことなので、むしろ、UTF-8-Mobile#KDDI-Bの方が本命なのかもしれません。

この辺の仕様については、もう少し議論が必要かもしれません。

SJIS-Mobile#*-PUAについては、混乱を避けるために、Unicode数値エンティティのサポート等で不都合がなければ基本的に削除しても良いかと思っています。(この辺はコメントいただければと思います。)

注意

ベンダのロゴ等、Unicode 6.0に収録されていない文字はサポートされません。
具体的には、Shift_JISで以下のコードの文字はサポートされません。

- DoCoMo: F975,F976,F978,F979,F984,F986,F9B1,F9B2
- KDDI: F370,F478,F486,F748,F794,F7A3,F7CF,F7D2
- SoftBank: F7B1, FBAA

絵文字領域以外の文字の変換は、通常の Unicode <-> UTF-8 の変換と同じにしています。

上記のUnicode 6.0で定義されない文字を除きマッピングの動作を確認していますが、
例外処理等の確認は十分ではないため、PHP 5.4.0 の正式リリースまでに十分な確認が必要です。