そのis_numeric()は適切ですか?

こんにちは。hnwです。今回はPHPの関数is_numericの使いどころについて問題提起をしてみます。また、PHPのソースコードを読んで、is_numericの実際の挙動を調べてみました。PHPのis_numericは、引数が「数値っぽいかどうか」をチェックする関数です。

is_numeric()って?

まずはリファレンスマニュアルを見てみましょう。

指定した変数が数値であるかどうかを調べます。数値文字列は以下の要素からなります。(オプションの)符号、任意の数の数字、(オプションの)小数部、そして(オプションの)指数部。つまり、+0123.45e6 は数値として有効な値です。16 進表記(0xFF)も認められますが、この場合は符号や小数部、指数部を含めることはできません。

PHPマニュアル – is_numeric

何が問題か

上のようにマニュアルに書いてあるにもかかわらず、ユーザーの入力値が10進整数値かどうかのチェックをしたいと思われる個所でis_numeric()を利用している例を見かけます。しかし、この関数は以下の文字列を受け取ってもtrueを返してしまいます。

  • 0xabcd
  • 12.34
  • 12e34
  • -.12e-34

これらの文字を与えてもtrueになることを忘れていないですか、というのが今回の問題提起です。もちろん、プログラム中で整数値を仮定しているパラメータがis_numeric()によるチェックをすり抜けて先の処理に進んだところで、セキュリティホールになる例は少ないでしょう。しかし、これが原因のバグというのは十分考えられるのではないでしょうか。

下記はネット上で見つけたコードを少しアレンジしたものです。

function validate_date($date) {
    $dateArr = explode("-", $date);
    if (count($dateArr) == 3 &&
        is_numeric($dateArr[0]) && strlen($dateArr[0]) == 4 &&
        is_numeric($dateArr[1]) && strlen($dateArr[1]) == 2 &&
        is_numeric($dateArr[2]) && strlen($dateArr[2]) == 2) {
          return checkdate($dateArr[1], $dateArr[2], $dateArr[0]);
    }
    return false;
}

例えば、上記のコードでvalidate_date(“12e3-12-31″)はtrueになりますが、この値をそのままSQL文の日付型の値として使うとSQLエラーになります。このコードはあくまでサンプルですが、似た状況はあり得るのではないでしょうか。

is_numeric()の詳細な挙動

細かい挙動が気になる人のために、以下にPHP 5.2.5でis_numeric()がtrueを返す条件を書き出してみました。(実はバージョンごとに挙動が異なる部分もあるのですが、下記のように把握をしておけば大抵の人にとっては十分だろうと思います)

  • 整数
  • 浮動小数点数
  • 全体が下記のいずれかの正規表現にマッチする文字列
    • [\x20\t\x0a-\x0d]*0[Xx][0-9A-Fa-f]+
    • [\x20\t\x0a-\x0d]*[\+\-]?[0-9]+([\.][0-9]*)?([Ee][\+\-]?[0-9]+)?
    • [\x20\t\x0a-\x0d]*[\+\-]?[\.][0-9]+([Ee][\+\-]?[0-9]+)?

ほぼマニュアルの通りの挙動です。マニュアルに書いていないことは、先頭の空白文字列を読み飛ばしてくれることくらいでしょうか。小数部が「3.」でも「.3」でも正しいというのはもしかすると意外に思われる方がいるかもしれませんが、これはPHPプログラム中に浮動小数点数を書く場合も同じように書けますので、それほど意外なことではありません。指数部も普通ですね。

まとめ

is_numeric()は文字列が16進数や浮動小数点数の形式であってもtrueを返します。知識としては知っている人が多いと思いますが、ついうっかり10進整数のチェックに使っていることはないでしょうか。

個人的には、本当にis_numeric()そのものが必要な状況というのは滅多に無いのではないかと思います。大抵の場合、正規表現で記述すれば十分なのではないでしょうか。[0-9]+にマッチするかどうかのチェック程度であれば、正規表現で記述した方がミスも無く保守性も高くなるはずです。もしくは、数値形式の文字列を受け取った直後に整数にキャストするのが良い場合もありそうです。

どうしてもis_numeric()が使いたいのであれば、is_numeric()がtrueを返した直後に整数なり浮動小数点数なりにキャストすれば問題が起こることは無いはずです。

コメント / トラックバック 1 件

#1 ==で文字列同士を数値として比較する条件 2008/01/15 14:45

[...] そんなわけで、「==演算子は両辺が文字列同士の場合に、両辺ともis_numeric()がtrueになるような文字列だった場合には数値として比較する」というのはマニュアルに書いてあることになりました。以前のis_numeric()を解説するエントリは「==」が文字列同士を数値比較する条件の説明にもなっているわけです。ご活用ください。 [...]

コメントする