がるの健忘録

エンジニアでゲーマーで講師で占い師なおいちゃんのブログです。

date_parseにおける「日付フォーマットにエラーがある場合」とは?

んと…大分前に嵌ったのですが。
「日付のvalidateをどうすんべ」ってのが発端でございました。


例えば

  $s = 'yyyymmdd';

という文字列をdate_parseすればエラーなのは当然でございます。
まぁ…
http://php.benscom.com/manual/ja/function.date-parse.php

返り値
成功した場合に日付情報を含む配列、 失敗した場合に FALSE を返します。

を一瞬読み間違えると

  $s = 'yyyymmdd';
  $ret = date_parse($s);
  if (false === $ret) {
    // エラー処理
  }

と書いてそれなりに痛い目にあうのですが orz


上述における「失敗した場合」というのは「パース結果が異常である」ではなくて「パース処理そのものが何らかの事情でこけた」です。
おいといて。


んじゃまぁってんで読み進めますと

日付フォーマットにエラーがある場合は、 'errors' 要素にエラーメッセージが格納されます。

とか書いてあります。

  $s = 'yyyymmdd';
  $ret = date_parse($s);
var_dump($ret);

で確かめてみますと、確かに

["warning_count"]=>
int(1)
["warnings"]=>
array(1) {
[6]=>
string(29) "Double timezone specification"
}
["error_count"]=>
int(1)
["errors"]=>
array(1) {
[0]=>
string(47) "The timezone could not be found in the database"
}

となっておりますふむり。
error_count使ってもよござんしょうし、errorsがどうなっているかをチェックしてもよござんしょうし。
よっしゃ日付のvalidateはこれでばっち「ぐ〜*1」とか考えると大きな落とし穴にはまります。

  $s = '99999999';
  $ret = date_parse($s);
var_dump($ret);

こんなコードで試してみましょう。

["warning_count"]=>
int(0)
["warnings"]=>
array(0) {
}
["error_count"]=>
int(0)
["errors"]=>
array(0) {
}

………まてこら。なんていうか…多分全国幾多のプログラマの予想とは反してるんじゃないかと思うのですが如何でしょうか?
ちなみに。validateをチェックする時に、本当の本当に見るべき場所は、ここ。

["year"]=>
int(9999)
["month"]=>
bool(false)
["day"]=>
bool(false)
["hour"]=>
bool(false)
["minute"]=>
bool(false)
["second"]=>
bool(false)

んと…「日付だけの時と日時の時とでチェックする場所が当然変わる」のですが、この辺をis_boolあたりで丹念にチェックする必要があります。


以前に、無駄に一瞬嵌ったので、思い出しつつめもり。
…相変わらずきしょい仕様だ orz

*1: (C)えどはるみ

なぜこうもレビューされてないコードを記事に書く?

元ネタは
わずか数行で"ものすごいテーブル"に! - jQueryプラグイン「Flexigrid」
http://journal.mycom.co.jp/articles/2008/06/25/flexigrid/menu.html
の三番目のPageである
動作サンプル - JSON+Ajaxでソートなどを可能にしたテーブルリスト
http://journal.mycom.co.jp/articles/2008/06/25/flexigrid/002.html
あえて。あえて、書かれてるソースをまず列挙してから会話をしたい。

<?php

function sanitizeTag($var)
{
    if ( is_array($var) )
    {
        $var = array_map("sanitizeTag", $var);
    }
    else
    {
        $var = htmlspecialchars(trim($var));
    }
    return $var;
}

$postdata = sanitizeTag($_POST);

$postdata['page'] = ( !$postdata['page'] ) ? 1 : $postdata['page'];
$postdata['rp'] = ( !$postdata['rp'] ) ? 10 : $postdata['rp'];
$postdata['sortname'] = ( !$postdata['sortname'] ) ? 'zip_code' : $postdata['sortname'];
$postdata['sortorder'] = ( !$postdata['sortorder'] ) ? 'asc' : $postdata['sortorder'];

try
{
    $dbh = new PDO
    (
        'mysql:host=localhost;dbname=zip',
        'root', // DB接続ユーザ名
        'password' //DB接続ユーザパスワード
    );

    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    $sql_field_array = array
    (
        'zip_code', // 郵便番号
        'pref_name', // 都道府県名
        'city_name', // 市区名
        'area_name' // 地名
    );

    $select_field_name = '';
    for($n=0; count($sql_field_array) > $n; $n++)
    {
        if ( 0 < $n )
        {
            $select_field_name .= ', ';
        }
        $select_field_name .= $sql_field_array[$n];
    }

    $where = ($postdata['qtype'] && $postdata['query']) ? 'where ' . $postdata['qtype'] . ' like \''. $postdata['query']. '\'' : '';

    $stmt = $dbh->query( 'select COUNT(*) from zip ' . $where );
    $count = $stmt->fetchColumn();

    $sql = 'select '. $select_field_name . ' from zip '.
        $where .
        'order by ' . $postdata['sortname'] . ' ' . $postdata['sortorder'] . ' ' .
        'limit ' . ( ( $postdata['page'] - 1 ) * $postdata['rp'] ) . ', ' . $postdata['rp']; 
    $stmt = $dbh->prepare( $sql );
    $stmt->execute();

    $json = array
    (
        'page' => $postdata['page'] - 0,
        'total' => $count,
    );

    while( $row = $stmt->fetch(PDO::FETCH_ASSOC) )
    {
        foreach($row as $key => $value)
        {
            $tmp[] = $value;
        }

        $json['rows'][] = 
        (
            array
            (
                'cell' => $tmp
            )
        );

        unset($tmp,$key,$value);
    }

    $stmt = null;

    echo json_encode($json);

}
catch (PDOException $e)
{
    var_dump($e);
}

?>

で、まずい部分を突っ込み。


まず

        $var = htmlspecialchars(trim($var));

これ。識者がどれだけか口を酸っぱくしてこの問題を声高に叫んでいる事か。
とりあえず、個人的にはこちらのpageのURIをあげさせていただきたく。


htmlspecialchars/htmlentitiesの正しい使い方
http://blog.ohgaki.net/htmlentitiesa_raspa_a_afia_a_s

htmlspecialcharsとhtmlenties関数はENT_QUOTESを指定しないとENT_COMPAT(セキュリティ上問題があるが互換性を維持)が指定された状態と同じ動作をします。


だ〜か〜ら〜「一枚ラップしろと」あれほどに以下略。trimも色々問題出そうだけど突っ込む気力もございませぬ。
ちなみにMWだとsecurityクラスでラップしておりますってのは余談。


つぎ。

$postdata = sanitizeTag($_POST);

これのまずい部分がわかるでしょうか?
答えは
エスケープは使う直前に仕様用途に合わせて
という原則が完全無欠に無視されてるから。
で、上述がまずい事がよくわかる続編が、このコード。

    $where = ($postdata['qtype'] && $postdata['query']) ? 'where ' . $postdata['qtype'] . ' like \''. $postdata['query']. '\'' : '';

    $stmt = $dbh->query( 'select COUNT(*) from zip ' . $where );

とか

    $sql = 'select '. $select_field_name . ' from zip '.
        $where .
        'order by ' . $postdata['sortname'] . ' ' . $postdata['sortorder'] . ' ' .
        'limit ' . ( ( $postdata['page'] - 1 ) * $postdata['rp'] ) . ', ' . $postdata['rp']; 
    $stmt = $dbh->prepare( $sql );

とか。


…あのですね。
以前、 http://d.hatena.ne.jp/gallu/20070628/p1 に書きましたが。
id:elfさんが書かれてますが。
http://d.hatena.ne.jp/elf/20070628/1183004732

$value = htmlspecialchars($value);

$sql = 'INSERT ... '.$value.'...';

とかよくみかけます.

いやSQL Injectionは防げるかもしれないけど(苦笑 「なぜエスケープしないといけないか?」ということを考えていない andor 理解していないことを,この1〜2行でこちら側に理解させるというコードですね.

こんな話が「失笑レベルの笑い話として」出ているわけですよ。
ちなみに。シングルクォートがエスケープされていない時点でSQL-Injection防げません orz
っつかね。とりあえずさぁ

$s = '"\';';
print $s . "\n";
$s = htmlspecialchars($s);
print $s . "\n";

こーゆーミニマムコード書いてみ? 何起きるかわかるべ?
わかんなきゃ周りの中にいる賢人に質問してみるべし。


結局のところ。XSSバリバリのSQL-Injectionダラリンコの、ソースとしては「最低」なレベルなわけです。
ちなみに。上述見つけた時点で、それ以上のチェックしてません。多分、ほかにも宝は埋もれてるんじゃないかと推測したりしなかったり。
まぁ一応。ご意見ご感想に連絡は入れましたけどね。この記事教えてくれた人も連絡いれたらしいですけどね。
…反応があったら追記しま。


とりあえず。頼むからこんな駄プログラム書かないでください*1 orz
っていうか。先日のZDNetでも思ったですが。ちったぁまともなレビュアーを入れるとか、せめて「ベータ版公開で広く突っ込まれてみてから本番公開」とか、なにがしかもうちょっと方法ってないもんですかねぇ?

*1:カテゴリがWeb2.0ってあたりですでに駄目な香り満載とかマイコムって昔「人の自宅の電話番号を無断でパスワードに設定しやがったよねぇ」な前歴とかまぁそもそも母体に思うところも多々ありますがおいといて。