2010-01-11
■[PHP] PHP 5.3.0 の日付処理クラスと関数の追加・変更について

PHP 5.3.0 が公開されたのは結構前ですが、日付関係の処理について、PHP 5.3.0 でクラスや関数の追加・変更がありましたので、気になった部分だけ調べてみました。
新しく追加されたクラスとメソッド
PHP 5.3.0 では、以下のクラスが追加されました。
- DateInterval - 日付の間隔を表わす
- DatePeriod - 日付の期間を表わす
DateTime クラスには、以下のメソッドが追加されました。
- DateTime::add() - 年月日時分秒の値を DateTime オブジェクトに加える
- DateTime::createFromFormat() - 指定した書式でフォーマットした新しい DateTime オブジェクトを返す
- DateTime::diff() - ふたつの DateTime オブジェクトの差を返す
- DateTime::getLastErrors() - 日付/時刻 の操作中に発生した最後の警告やエラーを返す
- DateTime::getTimestamp() - Unix タイムスタンプを取得する
- DateTime::sub() - 年月日時分秒の値を DateTime オブジェクトから引く
- DateTime::setTimestamp() - Unix タイムスタンプを用いて日付と時刻を設定する
詳細は、PHP マニュアルの PHP 5.2.x から PHP 5.3.x への移行で確認できます。追加されたクラスは、主に、追加されたメソッド(DateTime::add(), DateTime::diff(), DateTime::sub())の引数や返り値として使用されます。
使用例としては、以下の通りです。
<?php // 1年と3日後を求める $datetime = new DateTime( '2010-01-11' ); $datetime->add( new DateInterval( 'P1Y3D' ) ); // 1年と3日 echo "1. " . $datetime->format( 'Y-m-d' ) . "\n"; // 500日前を求める $datetime = new DateTime( '2010-01-11' ); $datetime->sub( new DateInterval( 'P500D' ) ); // 500日 echo "2. " . $datetime->format( 'Y-m-d' ) . "\n"; // 2010-01-11 から 2014-01-11 までの日数を求める $datetime1 = new DateTime( '2010-01-11' ); $datetime2 = new DateTime( '2014-01-11' ); echo "3. " . $datetime1->diff( $datetime2 )->format( '%a' ) . "\n";; // 2010-01-01 から 2010-01-31 までを 1週ごとにループ $start_date = new DateTime( '2010-01-01' ); $end_date = new DateTime( '2010-01-31' ); $interval = new DateInterval( 'P7D' ); // 7日間隔 foreach ( new DatePeriod( $start_date, $interval, $end_date ) as $d ) { echo "4. " . $d->format( "Y-m-d\n" ); }
結果は以下のようになります。
1. 2011-01-14 2. 2008-08-29 3. 1461 4. 2010-01-01 4. 2010-01-08 4. 2010-01-15 4. 2010-01-22 4. 2010-01-29
DatePeriod を使用すると、一定間隔で日付や時間を進めることができますので、カレンダーやスケジューラのようなアプリケーションの作成には便利かもしれません。
次月を取得する方法
Obtaining the next month in PHP によると、PHP 5.3.0 以降では、"first day of next month" という書き方ができるようになったそうです。
strtotime() や、DateTime クラスで、次月を取得する場合、最初に "next month" を使用することを思い付きますが、1/31 などを指定すると、正しく次の月にならない場合があります。
<?php $d = new DateTime( '2010-01-31' ); $d->modify( 'next month' ); echo $d->format( 'F' );
結果は、以下のように "March" となります。
March
PHP 5.3.0 からは、"first day of next month" という書き方で、"February" が取得できます。
<?php $d = new DateTime( '2010-01-31' ); $d->modify( 'first day of next month' ); echo $d->format( 'F' );
PHP 5.3.0 以降では、以下のように "February" が表示されます(PHP 5.2.x では、"March" が表示)。
February
DateTime::modify() が変更後の DateTime を返すようになった
PHP 5.3.0 からは、以下のように DateTime::modify() の後ろにメソッドを続けて書くことができるようになりました(参考: DateTime::modify)。
<?php $datetime = new DateTime( '2010-01-11' ); echo $datetime->modify( 'last day' )->format( 'Y-m-d' ) . "\n"; // 以下のような書き方も可能です echo date_create( '2010-01-11' )->modify( 'last day' )->format( 'Y-m-d' ) . "\n";
結果は以下のようになります(PHP 5.2.x ではエラーになります)。
2010-01-31 2010-01-31
指定した年月の第[1-5][日-土]曜日の日付を求める関数を書き直してみる
以前にも、指定した年月の第[1-5][日-土]曜日の日付を求める関数例を紹介したことがあるのですが、今回、strtotime() や DateTime::modify() メソッドで、使用可能なキーワードを調べてみたところ、もう少し簡単な方法を思い付いたので、書いておきたいと思います。
strtotime() を使用した場合は以下のように書くことができます。こちらは、PHP 4.4.9 でも動作することを確認しました。ただし、strtotime() には 2038-01-19 以降の日付は正しく取得できないなどの制限がある場合があります(PHP マニュアル strtotime の注意を参照)。
<?php /** * 指定した年月の第[1-5][日-土]曜日の日付を求める * strtotime() 使用版 */ function calc_day( $year, $month, $week_of_month, $day_of_the_week ) { $year_month = sprintf( '%04d-%02d', (int)$year, (int)$month ); $time = strtotime( (int)$week_of_month . ' ' . $day_of_the_week, strtotime( $year_month ) ); if ( $time < 0 || $time === FALSE || $year_month !== date( 'Y-m', $time ) ) { return FALSE; } $result = (int)date( "j", $time ); if ( ! checkdate( $month, $result, $year ) ) { return FALSE; } return $result; }
DateTime クラスを使用すると、以下のようになります。こちらの場合は、strtotime() のような制限がありません。PHP 5.2.0 以降を使用している場合は、DateTime クラスを使用した方が良いと思います。
<?php /** * 指定した年月の第[1-5][日-土]曜日の日付を求める * DateTime クラス使用版 */ function calc_day( $year, $month, $week_of_month, $day_of_the_week ) { $year_month = sprintf( '%04d-%02d', (int)$year, (int)$month ); $datetime = new DateTime( $year_month ); $datetime->modify( (int)$week_of_month . ' ' . $day_of_the_week ); if ( $year_month !== $datetime->format( 'Y-m' ) ) { return FALSE; } $result = (int)$datetime->format( "j" ); if ( ! checkdate( $month, $result, $year ) ) { return FALSE; } return $result; }
使用方法は以下の通りです。
<?php // 2010年1月の第1日曜日から第5日曜日までを求める echo calc_day( 2010, 1, 1, 'sun' ) . "\n"; echo calc_day( 2010, 1, 2, 'sun' ) . "\n"; echo calc_day( 2010, 1, 3, 'sun' ) . "\n"; echo calc_day( 2010, 1, 4, 'sun' ) . "\n"; echo calc_day( 2010, 1, 5, 'sun' ) . "\n";
結果は以下のようになります。
3 10 17 24 31
strtotime() や DateTime::modify() で使用可能なキーワード
strtotime() や DateTime::modify() では、英文形式の日付を解釈できることになっていますが、PHP マニュアルには、正式にどのようなキーワードが使用可能なのかが載っていません。PHP 5.3.x のソースには ext/date/lib/parse_date.re に strtotime() や DateTime::modify() で使用可能なキーワードが定義されています。一覧のキーワードと後ろの数値を見るだけで、ある程度、使用方法を想像できるのではないでしょうか。
static timelib_relunit const timelib_relunit_lookup[] = { { "sec", TIMELIB_SECOND, 1 }, { "secs", TIMELIB_SECOND, 1 }, { "second", TIMELIB_SECOND, 1 }, { "seconds", TIMELIB_SECOND, 1 }, { "min", TIMELIB_MINUTE, 1 }, { "mins", TIMELIB_MINUTE, 1 }, { "minute", TIMELIB_MINUTE, 1 }, { "minutes", TIMELIB_MINUTE, 1 }, { "hour", TIMELIB_HOUR, 1 }, { "hours", TIMELIB_HOUR, 1 }, { "day", TIMELIB_DAY, 1 }, { "days", TIMELIB_DAY, 1 }, { "week", TIMELIB_DAY, 7 }, { "weeks", TIMELIB_DAY, 7 }, { "fortnight", TIMELIB_DAY, 14 }, { "fortnights", TIMELIB_DAY, 14 }, { "forthnight", TIMELIB_DAY, 14 }, { "forthnights", TIMELIB_DAY, 14 }, { "month", TIMELIB_MONTH, 1 }, { "months", TIMELIB_MONTH, 1 }, { "year", TIMELIB_YEAR, 1 }, { "years", TIMELIB_YEAR, 1 }, { "monday", TIMELIB_WEEKDAY, 1 }, { "mon", TIMELIB_WEEKDAY, 1 }, { "tuesday", TIMELIB_WEEKDAY, 2 }, { "tue", TIMELIB_WEEKDAY, 2 }, { "wednesday", TIMELIB_WEEKDAY, 3 }, { "wed", TIMELIB_WEEKDAY, 3 }, { "thursday", TIMELIB_WEEKDAY, 4 }, { "thu", TIMELIB_WEEKDAY, 4 }, { "friday", TIMELIB_WEEKDAY, 5 }, { "fri", TIMELIB_WEEKDAY, 5 }, { "saturday", TIMELIB_WEEKDAY, 6 }, { "sat", TIMELIB_WEEKDAY, 6 }, { "sunday", TIMELIB_WEEKDAY, 0 }, { "sun", TIMELIB_WEEKDAY, 0 }, { "weekday", TIMELIB_SPECIAL, TIMELIB_SPECIAL_WEEKDAY }, { "weekdays", TIMELIB_SPECIAL, TIMELIB_SPECIAL_WEEKDAY }, { NULL, 0, 0 } }; /* The relative text table. */ static timelib_lookup_table const timelib_reltext_lookup[] = { { "first", 0, 1 }, { "next", 0, 1 }, { "second", 0, 2 }, { "third", 0, 3 }, { "fourth", 0, 4 }, { "fifth", 0, 5 }, { "sixth", 0, 6 }, { "seventh", 0, 7 }, { "eight", 0, 8 }, { "eighth", 0, 8 }, { "ninth", 0, 9 }, { "tenth", 0, 10 }, { "eleventh", 0, 11 }, { "twelfth", 0, 12 }, { "last", 0, -1 }, { "previous", 0, -1 }, { "this", 1, 0 }, { NULL, 1, 0 } }; /* The month table. */ static timelib_lookup_table const timelib_month_lookup[] = { { "jan", 0, 1 }, { "feb", 0, 2 }, { "mar", 0, 3 }, { "apr", 0, 4 }, { "may", 0, 5 }, { "jun", 0, 6 }, { "jul", 0, 7 }, { "aug", 0, 8 }, { "sep", 0, 9 }, { "sept", 0, 9 }, { "oct", 0, 10 }, { "nov", 0, 11 }, { "dec", 0, 12 }, { "i", 0, 1 }, { "ii", 0, 2 }, { "iii", 0, 3 }, { "iv", 0, 4 }, { "v", 0, 5 }, { "vi", 0, 6 }, { "vii", 0, 7 }, { "viii", 0, 8 }, { "ix", 0, 9 }, { "x", 0, 10 }, { "xi", 0, 11 }, { "xii", 0, 12 }, { "january", 0, 1 }, { "february", 0, 2 }, { "march", 0, 3 }, { "april", 0, 4 }, { "may", 0, 5 }, { "june", 0, 6 }, { "july", 0, 7 }, { "august", 0, 8 }, { "september", 0, 9 }, { "october", 0, 10 }, { "november", 0, 11 }, { "december", 0, 12 }, { NULL, 0, 0 } };
2009-12-20
■[PHP][mbstring] PHP 5.2.12 の文字エンコーディング関連の修正点

PHP 5.2.12 がリリースされました。
Release Announcement にも載っていますが、以前、日記に書いた htmlspecialchars() が Shift_JIS の一部の文字を通してしまう問題は、セキュリティ問題として修正されました。
他にもセキュリティ問題の修正がありますので、バージョンアップした方が安全です。PHP 5.2.12 では、htmlspecialchars() の他にも、mbstring 関連でも文字エンコーディング関連の問題が修正されましたので、メモしておきます。
htmlspecialchars()/htmlentities() で文字エンコーディングを指定した場合に、一部の不正な文字が排除されない問題の修正
以前の日記(最新の PHP スナップショットでの htmlspecialchars()/htmlentities() の修正内容について)に書いたように、修正されました。
最近になって、UTF-8 のサロゲートペアの問題も修正され、PHP 5.2.12 にもこの修正が含まれています。
また、PHP 5.3 系では、PHP 5.3.2 でこの修正が反映される予定です。
mb_convert_encoding() が UTF-16 の BOM 付き Little Endian の文字列を正しく変換できない問題の修正
以前の日記で書いた、UTF-16(BOM 付き Little Endian) を mb_convert_encoding() で変換すると文字列が壊れる問題が修正されました。
PHP 5.2.12 で修正され、PHP 5.3.2 でも反映される予定です。これで、UTF-16 を扱う場合でも、正しく変換できるようになりました。以下は PHP 5.2.12 の実行結果です。
<?php // テスト(UTF-16: BOM 付き Little Endian の文字列) $str = "\xFF\xFE\xC6\x30\xB9\x30\xC8\x30"; var_dump( bin2hex( mb_convert_encoding( $str, "UTF-16", "UTF-16" ) ) ); var_dump( mb_convert_encoding( $str, "UTF-8", "UTF-16" ) );
string(12) "30c630b930c8" string(9) "テスト"
mb_detect_encoding() の第3引数(strict)を有効にすると、文字エンコーディングの判定が厳格になる
mb_detect_encoding() は文字コード判定として使用できるかで書いた問題です。
Patch を PHP-dev メーリングリスト に投稿したところ、取り込んでいただけました。
この修正によって、以下のコードは、TRUE となりますが、
<?php // Shift_JIS として有効な先行バイトのみで判定 var_dump( "SJIS" === mb_detect_encoding( "\xa1", "SJIS" ) );
bool(true)
以下のコードは、FALSE になります(PHP 5.2.12/PHP 5.3.2 以降)。
<?php var_dump( "SJIS" === mb_detect_encoding( "\xa1", "SJIS", TRUE ) );
bool(false)
■[PHP] 文字エンコーディング判定スクリプト

最近、忙しかったのですが、久しぶりに少し時間があったので、随分前に書いた文字エンコーディング判定スクリプトを見直してみました。とりあえず、出来上がったものを投稿しておきます。
何か適当な日本語の文字列を与えると、JIS/eucJP-win/SJIS-win/UTF-8 からそれなりに妥当な文字エンコーディングを返します。短い文字列で、mb_detect_encoding() だけでは判定に失敗する場合に役立つかもしれません。PHP 5.2.12 での動作を確認しています。
<?php /** * 日本語文字列の文字エンコーディング判定(ASCII/JIS/eucJP-win/SJIS-win/UTF-8) */ function detect_encoding_ja( $str ) { $enc = mb_detect_encoding( $str, 'ASCII,JIS,eucJP-win,SJIS-win,UTF-8', TRUE ); switch ( $enc ) { case FALSE : case 'ASCII' : case 'JIS' : case 'UTF-16' : case 'UTF-8' : break; case 'eucJP-win' : // ここで eucJP-win を検出した場合、eucJP-win として判定 if ( mb_detect_encoding( $str, 'SJIS-win,UTF-8,eucJP-win', TRUE ) === 'eucJP-win' ) { break; } $_hint = "\xbf\xfd" . $str; // "\xbf\xfd" : EUC-JP "雀" // EUC-JP -> UTF-8 変換時にマッピングが変更される文字を削除( ≒ ≡ ∫ など) mb_regex_encoding( 'eucJP-win' ); $_hint = mb_ereg_replace( "\xad[\xe2\xf5\xf6\xf7\xfa\xfb\xfc\xf0\xf1\xf2\xf5\xf6\xf7\xfa\xfb\xfc]|" . "\x8f\xf3[\xfd\xfe]|\x8f\xf4[\xa1-\xa8\xab\xac\xad]|\x8f\xa2\xf1", '', $_hint ); $_tmp = mb_convert_encoding( $_hint, 'UTF-8', 'eucJP-win' ); $_tmp2 = mb_convert_encoding( $_tmp, 'eucJP-win', 'UTF-8' ); if ( $_tmp2 === $_hint ) { // 例外処理( EUC-JP 以外と認識する範囲 ) if ( // SJIS と重なる範囲(2バイト|3バイト|iモード絵文字|1バイト文字) ! preg_match( '/^(?:' . '(?:[\x8e\xe0-\xe9][\x80-\xfc])+|' . '(?:\xea[\x80-\xa4])+|' . '(?:\x8f[\xb0-\xef][\xe0-\xef][\x40-\x7f])+|' . '(?:\xf8[\x9f-\xfc])+|' . '(?:\xf9[\x40-\x49\x50-\x52\x55-\x57\x5b-\x5e\x72-\x7e\x80-\xb0\xb1-\xfc])+|' . '[\x00-\x7e]+' . ')+$/', $str ) && // UTF-8 と重なる範囲(全角英数字・記号|漢字|1バイト文字) ! preg_match( '/^(?:' . '(?:\xef[\xbc-\xbd][\x80-\xbf])+|(?:\xef\xbe[\x80-\x9f])+|(?:\xef\xbf[\xa0-\xa5])+|' . '(?:[\xe4-\xe9][\x8e-\x8f\xa1-\xbf][\x8f\xa0-\xef])+|' . '[\x00-\x7e]+' . ')+$/', $str ) ) { // 条件式の範囲に入らなかった場合は、eucJP-win として検出 break; } // 例外処理2(一部の頻度の多そうな熟語を eucJP-win として判定) // (狡猾|珈琲|琥珀|瑪瑙|碼碯|絨緞|耄碌|膃肭臍|薔薇|蜥蜴|蝌蚪) if ( preg_match( '/^(?:' . '\xe0\xc4\xe0\xd1|\xe0\xdd\xe0\xea|\xe0\xe8\xe0\xe1|\xe0\xf5\xe0\xef|' . '\xe2\xfb\xe2\xf5|\xe5\xb0\xe5\xcb|\xe6\xce\xe2\xf1|\xe9\xac\xe9\xaf|' . '\xe9\xf2\xe9\xee|\xe9\xf8\xe9\xd1|\xe7\xac\xe6\xed\xe7\xc1|' . '[\x00-\x7e]+' . ')+$/', $str ) ) { break; } } default : // ここで SJIS-win と判断された場合は、文字コードは SJIS-win として判定 $enc = mb_detect_encoding( $str, 'UTF-8,SJIS-win', TRUE ); if ( $enc === 'SJIS-win' ) { break; } $enc = 'SJIS-win'; // UTF-8 の記号と日本語の範囲の場合は UTF-8 として検出(記号|全角英数字・記号|漢字|1バイト文字) if ( preg_match( '/^(?:' . '(?:[\xc2-\xd4][\x80-\xbf])+|' . '(?:\xef[\xa4-\xab][\x80-\xbf])+|' . '(?:\xef[\xbc-\xbd][\x80-\xbf])+|' . '(?:\xef\xbe[\x80-\x9f])+|' . '(?:\xef\xbf[\xa0-\xa5])+|' . '(?:[\xe2-\xe9][\x80-\xbf][\x80-\xbf])+|' . '[\x09\x0a\x0d\x20-\x7e]+|' . ')+$/', $str ) ) { $enc = 'UTF-8'; } // UTF-8 と SJIS 2文字が重なる範囲への対処(SJIS を優先) if ( preg_match( '/^(?:[\xe4-\xe9][\x80-\xbf][\x80-\x9f][\x00-\x7f])+/', $str ) ) { $enc = 'SJIS-win'; } } return $enc; }
SJIS-win と UTF-8 が重なる部分の判定が難しいので、場合によってはうまく判定できない場合があります。
以下については、以前に書いた時と変わりません。
- EUC-JP のいわゆる半角カタカナ(\x8e[\xa1-\xfc])や、EUC-JP の一部の文字([\xe0-\xea][\xa1-\xfc])のみで構成される文字列は SJIS-win として判定される
- UTF-8 の一部の記号(数学記号とギリシア文字の一部)が含まれる文字列が eucJP-win として判定される
2009-11-01
■[PHP][mbstring] いわゆる半角カナや種依存文字などをメールで送信する方法

随分前から書こうと思っていて忘れていたのですが、役に立つ人がいるかもしれませんので、書いておきたいと思います。
メールの送信エンコーディングに UTF-8 を使用すれば、機種依存文字という問題を気にする必要はほとんどありませんが、まだ一般的には、ISO-2022-JP を使用してメールを送る方が多いと思います。ただ、PHP の mb_send_mail() では、いわゆる半角カタカナや、機種依存文字を送信することはできません。これらの文字は '?' に変換されます。
PHP 5.2.1 からは、レガシーエンコーディングの追加として、ISO-2022-JP-MS というエンコーディングが追加されました。ISO-2022-JP-MS を使用すれば、このエンコーディングでサポートされている文字を送信することができます。ISO-2022-JP-MS については、[PHP-dev 1345] PHP への CP932 系エンコーディングの追加パッチを参照してください。
ISO-2022-JP-MS を使用してメールを送信するには、mb_send_mail() ではなく、mail() を使用し、メールヘッダなどを定義します。例えば、関数化すると以下のようになります。
<?php function send_mail( $subject, $msg_body, $to_addr, $from_addr ) { // 言語設定は Japanese、内部文字エンコーディングは UTF-8 を使用していることを想定 // mb_language( 'Japanese' ); // mb_internal_encoding( 'UTF-8' ); $subject = mb_encode_mimeheader( $subject, 'ISO-2022-JP-MS', 'UTF-8' ); $mailto = $to_addr; $body = mb_convert_encoding( $msg_body, 'ISO-2022-JP-MS', 'UTF-8' ); $add_headers = "Content-Type: text/html; charset=ISO-2022-JP\n" . 'From:' . mb_encode_mimeheader( $from_addr, 'ISO-2022-JP-MS', 'UTF-8' ); return mail( $mailto, $subject, $body, $add_headers ); }
実用する場合は、Envelope From の設定や、メールアドレスチェックを行った方が安全です。また、受信側で必ずしもこれらの文字が正しく読めるかどうかは分からないことには注意してください。
2009-10-16
■[PHP][セキュリティ]最新の PHP スナップショットでの htmlspecialchars()/htmlentities() の修正内容について

前の記事(Shift_JIS では、htmlspecialchars() を使用しても XSS が可能な場合がある)で Shift_JIS ではブラウザによっては XSS が発生する可能性があることを書きました。この問題は、PHP の開発版のスナップショット(snaps.php.net)で修正されたことを確認しましたが、それ以外にも、EUC-JP や UTF-8 について修正が行われました。この件で、行動された方に感謝します。どうもありがとうございました。その後、修正内容について調べましたので、メモしておきます。他の修正点など、何か気付いた方がおられましたら、ぜひ教えてください。
この修正は、PHP 5.3.2/PHP 5.2.12 で反映されることになると思います。実際の修正内容の大半は、以下で行われました。
- http://svn.php.net/viewvc?view=revision&revision=289411
- http://svn.php.net/viewvc?view=revision&revision=289554
今後、まだ修正が行われる可能性がありますが、現時点(2009.10.16)で私が確認した変更内容は、以下の通りです。
- Shift_JIS
- \xf0 - \xfc が単独で指定された場合、空文字列を返すようになった
- Shift_JIS の先行バイト(\x81 - \x9f, \xf0 - \xfc)に続くバイト列が Shift_JIS として不正な場合、空文字列を返すようになった
- EUC-JP
- EUC-JP の先行バイト(\xa1 - \xfe)に続くバイト列が EUC-JP として不正な場合、空文字列を返すようになった
- UTF-8
- 冗長な表現が使用された場合、空文字列を返すようになった
- UTF-8 の 5, 6 バイト表現が指定された場合、空文字列を返すようになった
- U+10000 以降の一部の文字が不正に変換される問題が修正された(参考:htmlspecialchars/htmlentitiesはBMP外の文字を正しく扱えない)
以下は検証コードです。変更されなかった点についても書いておきました。
1. Shift_JIS
(1) \xf0 - \xfc が単独で指定された場合、空文字列を返すようになった
<?php var_dump( bin2hex( htmlspecialchars( "\xf0", ENT_QUOTES, "Shift_JIS" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xf1", ENT_QUOTES, "Shift_JIS" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xf2", ENT_QUOTES, "Shift_JIS" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xf3", ENT_QUOTES, "Shift_JIS" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xf4", ENT_QUOTES, "Shift_JIS" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xf5", ENT_QUOTES, "Shift_JIS" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xf6", ENT_QUOTES, "Shift_JIS" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xf7", ENT_QUOTES, "Shift_JIS" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xf8", ENT_QUOTES, "Shift_JIS" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xf9", ENT_QUOTES, "Shift_JIS" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xfa", ENT_QUOTES, "Shift_JIS" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xfb", ENT_QUOTES, "Shift_JIS" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xfc", ENT_QUOTES, "Shift_JIS" ) ) );PHP 5.3.0 / PHP 5.2.11 の結果です。
string(2) "f0" string(2) "f1" string(2) "f2" string(2) "f3" string(2) "f4" string(2) "f5" string(2) "f6" string(2) "f7" string(2) "f8" string(2) "f9" string(2) "fa" string(2) "fb" string(2) "fc"修正後(最新のスナップショット)の結果です。これらの文字列は出力されないようになりました。
string(0) "" string(0) "" string(0) "" string(0) "" string(0) "" string(0) "" string(0) "" string(0) "" string(0) "" string(0) "" string(0) "" string(0) "" string(0) ""
(2) Shift_JIS の先行バイト(\x81 - \x9f, \xf0 - \xfc)に続く1バイトが Shift_JIS として不正な場合、空文字列を返すようになった
<?php var_dump( bin2hex( htmlspecialchars( "\x81\x3e", ENT_QUOTES, "Shift_JIS" ) ) ); // \x3e は '>' var_dump( bin2hex( htmlspecialchars( "\x9f\x3e", ENT_QUOTES, "Shift_JIS" ) ) ); // \x3e は '>' var_dump( bin2hex( htmlspecialchars( "\xf0\x3e", ENT_QUOTES, "Shift_JIS" ) ) ); // \x3e は '>' var_dump( bin2hex( htmlspecialchars( "\xfc\x3e", ENT_QUOTES, "Shift_JIS" ) ) ); // \x3e は '>'PHP 5.3.0 / PHP 5.2.11 の結果です。
string(10) "812667743b" string(10) "9f2667743b" string(10) "f02667743b" string(10) "fc2667743b"修正後(最新のスナップショット)の結果です。これらの文字は出力されないようになりました。
string(0) "" string(0) "" string(0) "" string(0) ""
(3) \x80, \xa0, \xfd, \xfe, \xff については、そのまま出力される
<?php var_dump( bin2hex( htmlspecialchars( "\x80", ENT_QUOTES, "Shift_JIS" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xa0", ENT_QUOTES, "Shift_JIS" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xfd", ENT_QUOTES, "Shift_JIS" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xfe", ENT_QUOTES, "Shift_JIS" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xff", ENT_QUOTES, "Shift_JIS" ) ) );PHP 5.3.0 / PHP 5.2.11 の結果です。
string(2) "80" string(2) "a0" string(2) "fd" string(2) "fe" string(2) "ff"修正後(最新のスナップショット)の結果です。これらの文字列については変更ありません。
string(2) "80" string(2) "a0" string(2) "fd" string(2) "fe" string(2) "ff"
2. EUC-JP
(1) EUC-JP の先行バイト(\x8e, \x8f, \xa1 - \xfe)に続くバイト列が EUC-JP として不正な場合、空文字列を返すようになった
<?php var_dump( bin2hex( htmlspecialchars( "\x8e\x3e", ENT_QUOTES, "EUC-JP" ) ) ); // \x3e は '>' var_dump( bin2hex( htmlspecialchars( "\x8f\x3e\x3c", ENT_QUOTES, "EUC-JP" ) ) ); // \x3e は '>', \x3c は '<' var_dump( bin2hex( htmlspecialchars( "\xa1\x3e", ENT_QUOTES, "EUC-JP" ) ) ); // \x3e は '>' var_dump( bin2hex( htmlspecialchars( "\xfe\x3e", ENT_QUOTES, "EUC-JP" ) ) ); // \x3e は '>'PHP 5.3.0 / PHP 5.2.11 の結果です。
string(10) "8e2667743b" string(18) "8f2667743b266c743b" string(10) "a12667743b" string(10) "fe2667743b"修正後(最新のスナップショット)の結果です。これらの文字列は出力されないようになりました。
string(0) "" string(0) "" string(0) "" string(0) ""
(2) \x80 - \x8d, \x90 - \xa0, \xff については、そのまま出力される
<?php var_dump( bin2hex( htmlspecialchars( "\x80", ENT_QUOTES, "EUC-JP" ) ) ); var_dump( bin2hex( htmlspecialchars( "\x8d", ENT_QUOTES, "EUC-JP" ) ) ); var_dump( bin2hex( htmlspecialchars( "\x90", ENT_QUOTES, "EUC-JP" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xa0", ENT_QUOTES, "EUC-JP" ) ) ); var_dump( bin2hex( htmlspecialchars( "\xff", ENT_QUOTES, "EUC-JP" ) ) );PHP 5.3.0 / PHP 5.2.11 の結果です。
string(2) "80" string(2) "8d" string(2) "90" string(2) "a0" string(2) "ff"修正後(最新のスナップショット)の結果です。これらの文字列については変更ありません。
string(2) "80" string(2) "8d" string(2) "90" string(2) "a0" string(2) "ff"
3. UTF-8
(1) 冗長な表現が使用された場合、空文字列を返すようになった
<?php var_dump( bin2hex( htmlspecialchars( "\x3c", ENT_QUOTES, "UTF-8" ) ) ); // U+003c ('<' の冗長でない表現) var_dump( bin2hex( htmlspecialchars( "\xc0\xbc", ENT_QUOTES, "UTF-8" ) ) ); // U+003c ('<' の2バイト表現) var_dump( bin2hex( htmlspecialchars( "\xe0\x80\xbc", ENT_QUOTES, "UTF-8" ) ) ); // U+003c ('<' の3バイト表現) var_dump( bin2hex( htmlspecialchars( "\xf0\x80\x80\xbc", ENT_QUOTES, "UTF-8" ) ) ); // U+003c ('<' の4バイト表現) var_dump( bin2hex( htmlspecialchars( "\xf8\x80\x80\x80\xbc", ENT_QUOTES, "UTF-8" ) ) ); // U+003c ('<' の5バイト表現) var_dump( bin2hex( htmlspecialchars( "\xfc\x80\x80\x80\x80\xbc", ENT_QUOTES, "UTF-8" ) ) ); // U+003c ('<' の6バイト表現)PHP 5.3.0 / PHP 5.2.11 の結果です。
string(8) "266c743b" string(8) "266c743b" string(8) "266c743b" string(8) "266c743b" string(8) "266c743b" string(8) "266c743b"修正後(最新のスナップショット)の結果です。冗長な表現の場合は出力されなくなりました。
string(8) "266c743b" string(0) "" string(0) "" string(0) "" string(0) "" string(0) ""
(2) UTF-8 の 5, 6 バイト表現が指定された場合、空文字列を返すようになった
<?php var_dump( bin2hex( htmlspecialchars( "\xf8\xbf\xbf\xbf\xbf", ENT_QUOTES, "UTF-8" ) ) ); // U+3FFFFFF (UTF-8 では 5バイトで表現) var_dump( bin2hex( htmlspecialchars( "\xfc\xbf\xbf\xbf\xbf\xbf", ENT_QUOTES, "UTF-8" ) ) ); // U+7FFFFFFF (UTF-8 では 6バイトで表現)PHP 5.3.0 / PHP 5.2.11 の結果です。
string(10) "f8bfbfbfbf" string(12) "fcbfbfbfbfbf"修正後(最新のスナップショット)の結果です。5バイト、6バイトの表現は出力されなくなりました。
string(0) "" string(0) ""
(3) U+10000 以降の一部の文字が不正に変換される問題が修正された
(4) サロゲートペア(U+D800 - U+DFFF)の領域は、そのまま出力される
(4) サロゲートペア(U+D800 - U+DFFF)の領域を指定すると、空文字列を返すようになった(2009.12.08)
<?php var_dump( bin2hex( htmlspecialchars( "\xed\xa0\x80", ENT_QUOTES, "UTF-8" ) ) ); // U+D800 var_dump( bin2hex( htmlspecialchars( "\xed\xbf\xbf", ENT_QUOTES, "UTF-8" ) ) ); // U+DFFFPHP 5.3.0 / PHP 5.2.11 の結果です。
string(6) "eda080" string(6) "edbfbf"修正後(2009.12.06 以前のスナップショット)の結果です。これらの文字列についてはそのまま出力されます。
string(6) "eda080" string(6) "edbfbf"2009.12.07 以降のスナップショットでは、これらの文字列については出力されなくなりました。
string(0) "" string(0) ""
2009.12.08 追記
id:moriyoshi さんからサロゲートペアの問題も回避するようになったというコメントをいただきました。2009.12.07 以降のスナップショットを使用すれば、サロゲートペアの領域は出力されなくなります。
2009-10-04
■[PHP][セキュリティ] Shift_JIS では、htmlspecialchars() を使用しても XSS が可能な場合がある

以下のページに関連して、htmlspecialchars() を使用している場合でも XSS が可能かどうか少し調べてみました。
その結果、いくつかのブラウザで文字エンコーディングに Shift_JIS を使用していた場合、XSS が可能なことを確認しました。
テストコードは以下の通りです。リンクにマウスポインタを乗せると埋め込んだ Javascript が実行されます。
<?php $_GET['a1'] = "\xf0"; // \xf0 - \xfc で可能 $_GET['a2'] = " href=dummy onmouseover=alert(document.title) dummy=dummy"; header( "Content-Type:text/html; charset=Shift_JIS" ); ?> <html> <head><title>Shift_JIS test</title></head> <body> <p><a title="<?php echo htmlspecialchars( $_GET['a1'], ENT_QUOTES, 'SJIS' ) ?>" href="<?php echo htmlspecialchars( $_GET['a2'], ENT_QUOTES, 'SJIS' ) ?>">test</a></p> </body> </html>
上記のコードで XSS が発生するのは、以下が原因となるようです。
- PHP の htmlspecialchars() では、SJIS(Shift_JIS) の場合、\xf0 - \xfc を単独で指定しても排除しない(PHP 5.3.0 で確認)
- 一部のブラウザでは、\xf0 - \xfc に続く1バイトを合わせて Shift_JIS の1文字として認識する(Mozilla Firefox 3.5.3 と Internet Explorer 8(8.0.6001.18813) で確認)
- このため、上記の例では、\xf0 の後ろにある "(ダブルクォート) が有効にならず、その後ろに任意の属性が追加できる
このため、ブラウザによっては、XSS が可能な場合があります。
他のブラウザとして、Google Chrome 3.0.195.24 で確認してみましたが、\xf0 - \xfc の後ろに不正な文字が続く場合では、XSS は発生しませんでした。
追記(2009.10.06)
コメントで指摘いただいたように、Shift_JIS の先行バイトに関連する記述が正しくなかったため、表現を修正しました。
http://news.php.net/php.cvs/61170