PHPのクエリパラメーターの扱い方について

3年ぶりの更新というわけで、まさしく3年寝太郎状態なわけですが。

今日は今巷を賑わせているJSON SQL Injectionについてです。

徳丸さんの記事で紹介されていたように、僕が作ったSQL_Abstractでも同様の問題が発生するとのことです。

SQL::Makerのようにstrictモードを導入するかどうかも一応考えたのですが、PHPSQL_Abstract自体PHP5でも動作はしますがPHP4時代のコードですし、そもそもそんなに使われてないだろう(泣)いうことで今回は見送ることにしました。(ちょっと今すぐ時間が取れないという状況もありまして・・・)

またstrictモードを導入したとしてもSQL_Abstractを配列で使いたいケースが結構多いと思うので結局strictモードを外して運用になるのではないかと思うので効果としては薄いのかなぁとも感じています。

でまぁSQL_Abstractではstirctモード導入しないよ!勝手に対策してよ!だとあまりに無責任な気がするので、この問題に対して僕が実際にやってる対策の一つをここで紹介しておきます。

PHPのクエリが配列で取れてしまう問題というのは、これはSQL_Abstractとの相性の問題というよりは、クエリが配列で来くることを想定していないのが問題であり、そこの部分を根本的に解決しない限り他のあらゆる箇所でバグが発生する原因になるとおもいます。

徳丸さんの記事では

strictモードが使えない場合は、条件設定に与える引数の型チェックを行う方法があります。

      $user_name = $_GET['user_name'];
      if (! is_string($user_name)) {
        # エラー処理
        exit;
      }

このようにis_string等を使ってチェックする必要があるとのことでしたが、僕は根っからの面倒臭がり屋なのでいちいち毎回チェックしてエラー処理したりするのが面倒なので面倒を避けるために以下のように処理しています。

<?php

// そのまま返すやーつ
function get_param($key) {
    if ( isset($_GET[$key]) ) {
        return $_GET[$key];
    }
    return null;
}

// 常にひとつだけ返すやーつ
function get_value($key) {
    return _get_scalar(get_param($key));
}

function _get_scalar($param) {
    return is_array($param) ? _get_scalar(array_shift($param)) : $param;
}

// hoge.php?user_name[<>]=1とか渡されても大丈夫!
$where = array(
    'user_name'=> get_value('user_name')
);

上記のようなラップ関数を用意し、get_value関数を使ってクエリパラメータがどういう状態で渡されても常にひとつの値だけを取る様にしてます。



以上、ありあしたッ!

MDB2で複数のDB接続を行う場合の恐ろしい罠

MDB2を使って複数のDB接続したいなんてことあるよね。

ところがどっこい、実際にコードを書いてみると恐ろしいことが起きた。それは二つ目のDB接続をすると一つ目のDB接続が二つ目のDB接続に乗っ取られてしまうという!

<?php
// fooというデータベースへ接続し、hoge_tblのカウントする
$dbh = MDB2::factory('mysql://user:pass@localhost/foo?charset=utf8');
$res = $dbh->query('SELECT COUNT(*) FROM hoge_tbl');
$row = $res->fetchRow();
$res->free();
print $row[0]; // 5件

// で次にbarへ接続し同様の処理
$dbh2 = MDB2::factory('mysql://user:pass@localhost/bar?charset=utf8');
$res = $dbh2->query('SELECT COUNT(*) FROM hoge_tbl');
$row = $res->fetchRow();
$res->free();
print $row[0]; // 2件

// そして、fooに接続してるDBハンドルでもういちどカウントしてみると・・・
$res = $dbh->query('SELECT COUNT(*) FROM hoge_tbl');
$row = $res->fetchRow();
$res->free();
print $row[0]; // 2件

このコードが5、2、2と表示されるわけです。なんでこんなことに・・・、と思って色々調べてみると、どうやら同一ホストへの接続の場合、いろいろキャッシュ的な感じでごにょごにょむにょむにょして同一の接続扱いになってしまうようです。

でまぁ原因はわかったけど、じゃあどうやって複数接続すればいいんだよ!ということで、これを明示的に別の接続として扱うためのnew_linkってオプションを発見。

<?php
// new_link=trueをつけましょう!
$dbh2 = MDB2::factory('mysql://user:pass@localhost/bar?charset=utf8&new_link=true');

こうすることで二つ目のDB接続は新しい接続だよ!と明示され、晴れて同一の接続じゃなくなりました。

いやぁnew_linkなんていう便利なオプションがあるんですね() # デフォでtrueにしとけよ!って感じですが。

ということで今頃気付いたのかよって思う人もいるかもしれませんが、知らなかった人はもしかしたら気付かない間に別のDBに接続されててバグだらけになってた、なんてことになってるかもしれないのでご愁傷様です。

クラス内で定義した定数の一覧を得る方法

<?php

class Foo {
    const BAR = 1;
    const BAZ = 2;
}

get_defined_constantsを使えば全ての定数が取れると思いきや、上記のようなクラス内で定義した定数は取れなかった。

どうやったら取れるのか。PHPお得意の大量の関数の中に一覧取れるヤツが必ずあるはず!と思いきや、なかった。

諦めかけたその時、ReflectionClassというクラスを使えば取れることがわかったのだ!

<?php

class Foo {
    const BAR = 1;
    const BAZ = 2;
}
$reflect = new ReflectionClass('Foo');
print_r( $reflect->getConstants() );
$ php test.php
Array
(
    [BAR] => 1
    [BAZ] => 2
)

PHP5になってから、PHPさんは関数からの脱却を図っている模様だ。

目的のものが標準関数に存在しなかったら、標準クラスを調べてみようというお話でした。

Text::ASCIITableによるアスキーテーブルレイアウト

MySQLの出力結果みたいなのが必要になったのでCPAN漁ってたらこんなの発見。

Text::ASCIITable - Create a nice formatted table using ASCII characters. - metacpan.org

地味に凄いめちゃ便利

use Text::ASCIITable;

my @rows = (
    [1,'foo','2011-03-16 11:22:33'],
    [2,'hogehogehoge','2011-03-17 11:22:33'],
    [3,'uwaaaaa','2011-03-18 11:22:33'],
    [4,'dio','2011-03-19 11:22:33'],
    [5,'jojo','2011-03-20 11:22:33'],
);

my $t = Text::ASCIITable->new();
$t->setCols('id','name','created');
$t->addRow($_) for @rows;
print $t;
 $ perl asciitable.pl
 .-----------------------------------------.
 | id | name         | created             |
 +----+--------------+---------------------+
 |  1 | foo          | 2011-03-16 11:22:33 |
 |  2 | hogehogehoge | 2011-03-17 11:22:33 |
 |  3 | uwaaaaa      | 2011-03-18 11:22:33 |
 |  4 | dio          | 2011-03-19 11:22:33 |
 |  5 | jojo         | 2011-03-20 11:22:33 |
 '----+--------------+---------------------'

いいですねー。

あとサクサクっとハッシュ配列構造のデータを出力する場合はこんなかんじか

use Text::ASCIITable;

my @rows = (
    {'id' => 1,'name' => 'foo', 'created' => '2011-03-16 11:22:33'},
    {'id' => 2,'name' => 'hogehogehoge','created' => '2011-03-17 11:22:33'},
    {'id' => 3,'name' => 'uwaaaaa','created' => '2011-03-18 11:22:33'},
    {'id' => 4,'name' => 'dio','created' => '2011-03-19 11:22:33'},
    {'id' => 5,'name' => 'jojo','created' => '2011-03-20 11:22:33'},
);

if ( @rows ) {
    my @keys = keys %{$rows[0]};
    my $t = Text::ASCIITable->new();
    $t->setCols(@keys);
    $t->addRow(@$_{@keys}) for @rows;
    print $t;
}

いいですねー。

他にもオプション等で色々指定できるみたいですが、とりあえずこれだけできりゃもうおkですね。

PHP5.2とPHP5.3でArrayObjectの挙動が違う話

ハマッタのでメモ。

<?php

$a = new ArrayObject(array('test' => 1));

print_r($a);

これをPHP5.2とPHP5.3で実行すると以下のようになる

# PHP5.2
ArrayObject Object
(
    [test] => 1
)

# PHP5.3
ArrayObject Object
(
    [storage:ArrayObject:private] => Array
        (
            [test] => 1
        )

)

PHP5.3の場合、なにやらデータの持ち方が新しくなったのかstorage:ArrayObject:privateとか言うのが増えてる。

これはおそらくstorageというprivate変数に配列のデータを持つようになったという感じか。まぁそれ自体は問題ない。

問題は以下のコード

<?php

$a = new ArrayObject(array('test' => 1));
$b = new ArrayObject($a);

print_r($b);

実行結果

# PHP5.2
ArrayObject Object
(
    [test] => 1
)

# PHP5.3
ArrayObject Object
(
    [storage:ArrayObject:private] => ArrayObject Object
        (
            [storage:ArrayObject:private] => Array
                (
                    [test] => 1
                )

        )

)

PHP5.2のときにはArrayObjectにArrayObjectを渡しても配列として処理されていたが、PHP5.3からはArrayObjectがネストされてしまう。

正直ArrayObjectってのは配列をシュミレートするもののはずなのでArrayObjectをオブジェクトとして保存してしまうPHP5.3の仕様には聊か首を捻らざるを得ない。

さらに怖いのが、この状態でもちゃんと配列として動いてしまうのでネストされてることに気付かないということだ。

<?php

$a = new ArrayObject(array('test' => 1));
$b = new ArrayObject($a);

print($b['test']); # ちゃんと「1」と表示される

つまり普通に扱ってる分には配列として動いてくれるので、めちゃくちゃネストしちゃっててもぜんぜん気付かない。

気付いたときには

ArrayObject Object
(
    [storage:ArrayObject:private] => ArrayObject Object
        (
            [storage:ArrayObject:private] => ArrayObject Object
                (
                    [storage:ArrayObject:private] => ArrayObject Object
                        (
                            [storage:ArrayObject:private] => ArrayObject Object
                                (
                                    [storage:ArrayObject:private] => ArrayObject Object
                                        (
                                            [storage:ArrayObject:private] => ArrayObject Object
                                                (
                                                    [storage:ArrayObject:private] => ArrayObject Object
                                                        (
                                                            [storage:ArrayObject:private] => Array
                                                                (
                                                                    [test] => 1
                                                                )

                                                        )

                                                )

                                        )

                                )

                        )

                )

        )

)

こんなことになってるかもしれない。(ってかなってたw)

ArrayObjectを良く使う人はPHP5.3では注意しましょう!

ちなみに解決方法としては色々あるだろうけど、ArrayObjectに渡すときにarrayにキャストするとかしましょう。

<?php

$a = new ArrayObject(array('test' => 1));
$b = new ArrayObject((array)$a); // array型へキャスト

「Webを支える技術」読了

久しぶりに本を読みませうと神の意思が降りてきたので第一弾は「Webを支える技術」を読むことに。


Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)


なんせ前評判は高かったし、発売後も高評価続出だしとずっと読まなきゃなーと思ってた一冊です。

いやーーーー、ようやく読み終わった。

なかなか読み応えのある素晴らしい書籍でした。これは素晴らしいとしか言いようがないね。うん素晴らしい。

個人的に一番面白かったのが第2章のWebの歴史。

今までまったく意識してなかったのでなるほどなるほどそういう歴史的理由があったのかとかブツブツ独り言言いながら楽しく読めました。

優れた音楽家は、その楽曲が作られた当時の時代背景や作曲家の生まれ育った環境等を勉強することで、より良い演奏ができるようになる、という話に似ているような気がします。って音楽家の話は勝手に今思いついただけですが。

とにかくWebプログラマなら必読の一冊と言えるでしょう。

ただし、まったくの初心者にはオススメしません。

Webプログラムの実践経験が無い人がこの本を読んでも書いてある意味をなんとなくは理解できても実際にどう適用されるのか何が何だかさっぱり分からないと思うので、ある程度実際に経験を積んでから読むのが理想的かなぁと思います。

そうそう話変わって、なんか梅田に国内最大級の本屋がオープンしたみたいなので近々ちょっと行ってみようかな。

Class::Accessor::Fastが破壊的だったと初めて知ったあの日

事の発端はぽけーっとはてブのお気に入りを見ていたらClass::Accessor::Liteの記事が目に付いた事でした。

お、新しいモジュールか?と思いさっそく実装を拝見させてもらったわけです。

なるほどなるほど、超が付くほどの超シンプル。超々シンプル。実際問題Class::Accessor::Fast使うよりも、Liteのように自分で超軽量のアクセサ定義することの方が多かったりします。

しかし一点気になったところがありました。

@_のリファレンスを保存している部分があったのです。

# Class::Accessor::Lite-0.02

sub __m {
    my $n = shift;
    sub {
        return $_[0]->{$n} if @_ == 1;
        return $_[0]->{$n} = $_[1] if @_ == 2;
        shift->{$n} = \@_; ### ← この部分
    };
}

何故リファレンスで保存してるのかなぁと思い、大元のClass::Accessor::Fastではどうなってるのか実装を見てみたところ、同じく@_のリファレンスを保存していました。

# Class::Accessor::Fast-0.34

sub make_wo_accessor {
    my($class, $field) = @_;

    return sub {
        if (@_ == 1) {
            my $caller = caller;
            $_[0]->_croak("'$caller' cannot access the value of '$field' on objects of class '$class'");
        }
        else {
            return $_[0]->{$field} = $_[1] if @_ == 2;
            return (shift)->{$field} = \@_; ### ← この部分
        }
    };
}

言わずもがな@_というのは関数呼び出し時の引数として渡される変数のエイリアスとなるため、@_のリファレンスをメンバに保存して使いまわしてしまうと、色々と問題が出る可能性があります。

以下のコードは、大元の引数を破壊してしまう例です。

package Foo;
use base qw/Class::Accessor::Fast/;
Foo->mk_accessors('foo');

my $foo = Foo->new;
my @data = (10,20,30);

$foo->foo(@data); # @_は@dataのエイリアスになる

$foo->foo->[0] = 99; # メンバの方を99に変更する

print $data[0]; # 「99」と表示される!

ぶっちゃけこれは意図した仕様なのかどうかちょっとわかりかねますが、本家本元のClass::Accessorの実装ではリファレンスを保存していませんでした。

# Class::Accessor-0.34

sub set {
    my($self, $key) = splice(@_, 0, 2);

    if(@_ == 1) {
        $self->{$key} = $_[0];
    }
    elsif(@_ > 1) {
        $self->{$key} = [@_]; ### ← 無名配列に展開して保存してる
    }
    else {
        $self->_croak("Wrong number of arguments received");
    }
}

ということで恐らくはFast(及び、Fastを参考にしたLite)特有のバグの可能性は濃厚な感じがするっぽいノリです。

ただまぁそもそもアクセサに複数の値を渡すと配列のリファレンスで保持されるって言う仕様を、今初めて知ったんですけどねッ!





と、ここまで書いてる途中でようやくLiteの記事が2年前の記事だったことに気付いたわけで、今更2年前の記事に反応かよ的な冷たい視線も感じつつも、折角ここまで調べて文章化したのでネタもないしお蔵入りもったいないからあっぷあっぷ。