T

高速が売りのYii

新年早々、Yii PHP Frameworkを試している。Ruby on Rails以来、PHPでもウェブアプリケーションに使えるフレームワークは雨後の筍のように登場しているが、Rubyで実現できることをPHPで同じようにやろうとしても根本的に無理があるせいで、いずれもデファクトスタンダードになるにはどこか欠けた状態であり、したがって後発であればあるほど有利な状況だ。

そんな中で、このYiiは高速性を売りにしているところが他とちょっと違っている。いわく、

How is Yii Compared with Other Frameworks?

Like most PHP frameworks, Yii is an MVC framework.

Yii excels over other PHP frameworks in that it is efficient, feature-rich and clearly-documented. Yii is carefully designed from the beginning to fit for serious Web application development. It is neither a byproduct of some project nor a conglomerate of third-party work. It is the result of the authors’ rich experience of Web application development and the investigation and reflection of the most popular Web programming frameworks and applications.

サードパーティーの複合体じゃない、何かのバイプロダクトでもない、これ自体で製品レベルのウェブアプリ開発用フレームワークでございます、というわけだ。

環境設定だが、Yiiで用意されているチェック用スクリプト(requirements/index.php)によれば、PHP側でmemcache、PDO、APC、Mcryptなどの拡張機能が有効になっていることが要求される。この時点でさっきのサードパーティーがどうこうというのが破たんしている気がするが、とりあえずいろいろリビルドしてみる。

準備が完了したらさっそくチュートリアルに従ってテストアプリケーションを作成する。YiiRootはYiiをインストールした先と読み替える。

$ cd /YiiRoot/
$ chmod +x framework/yiic
$ ./framework/yiic webapp ./testdrive

自動生成ツールを使ってみる。存在しないディレクトリを指定しているのでディレクトリを作成するかどうか尋ねられる。Yesを選択すると、ずらずらとメッセージが表示されて何やら出来たらしい。

ブラウザでhttp://example.com/testdrive/index.phpにアクセスすると、確かにそれらしい画面が表示されている。正直な感想としては、うーん、デザインはあまりいけてないな。ナビゲーション用バーにHomeと並んでLoginというメニューがあるのでそちらを叩いてみると、確かにCAPTCHAを備えた何やらログイン画面が表示される。

demo/demoまたはadmin/adminでログインできる。内容に間違いがあるとエラー画面も出力される。

とはいえ、まだデータベースを設定しているわけでもないので、たんなるデモ以上の機能はない。さっそくデータベースの設定をする。が、YAMLで設定を書いてrakeでマイグレーション、みたいなおしゃれな機能はないみたいだ。DBを使う場合はPDOがインストール済みである必要がある。

MySQL上にtestdriveというデータベースを作成したら、

CREATE TABLE User(
    id integer NOT NULL PRIMARY KEY auto_increment,
    username VARCHAR(128) NOT NULL,
    password VARCHAR(128) NOT NULL,
    email VARCHAR(128) NOT NULL
);

という内容のテーブルを作成する。で、YiiRoot/testdrive/protected/config/main.phpの26行目あたりの配列「db」を直してMySQL用のDSNやらユーザ名やらを設定する。mysql_userは実際のMySQLのユーザ名、mysql_passは、まあわかるでしょ。

'db'=>array(
                        'connectionString'=>'mysql:dbname=testdrive',
                        'username' => 'mysql_user',
                        'password' => 'mysql_pass',
                        'emulatePrepare' => true,
                ),

最後の「emulatePrepare」という呪文は、この後で発生するエラーを防ぐもの。というのも、手順通りここから

$ cd YiiRoot/testdrive
$ YiiRoot/framework/yiic shell

でインタラクティブシェルを起動して、

>> model User

と入力して対応するモデルを作成するとチュートリアルには書いてあるが、うちのCentOS 4みたいにMySQLが4系の場合は「exception ‘CDbException’ with message ‘CDbCommand failed to execute the SQL statement: SQLSTATE[HY000]: General error: 2030 This command is not supported in the prepared statement protocol yet’」などなどなどの例外が発生する。これは文字通り利用中のMySQLがまだプリペアードステートメントをサポートしていないのが原因なので、先ほどの「emulatePrepare」をtrueに設定することで回避することができる。

で、ここからがYiiの売りのひとつ、crud(create、read、update、delete)用スクリプトの自動作成。crudというのは、ようするにウェブアプリケーション開発者ならこれまで数千回くらい手がけてきた、データの新規作成、一覧、更新、削除の一連の流れのこと。

やり方は、先ほどのシェルから

>> crud User

と入力するだけ。いくつかのスクリプトが作成される。

作成したモデルにアクセスするには、http://example.com/testdrive/index.php?r=userのようにGETのrで指定した小文字のモデル名のURLを利用する。この時点ではまだSEOコンサルを喜ばせるかっちょいいURIはない。さっそくアクセスすると、「New User」と「Manage User」のリンクが表示された何もないページが出てくる。問答無用でNew Userをクリックして試しにデータを作成してみる。

ユーザ名はRoy Rogers、パスワードも同じ、メールアドレスはRoyRogers@example.comに。まだパスワード入力用フォームは平文で入力した内容がそのまま表示されてしまうようだ。

無事完了したら、先ほどのhttp://example.com/testdrive/index.php?r=userに戻って、アカウントが作成されたことを確認しよう。

ちなみにdemo/demoでログインいていたらManage Userをクリックするとエラーになってしまった。admin/adminだと動作する。アカウント種別があるようだ。

チュートリアルはここまで。

MySQLのカラム複製脆弱性

昨日書いたMySQLの脆弱性ですが、手元の環境で見事に再現しました。MySQLのバージョンはCentOSでyumからインストールしたmysql-4.1.20-3.RHEL4.1.el4_6.1です。

手順は以下の通り。まずデータベース、テーブルを作成します。

mysql> create database sec_test;
Query OK, 1 row affected (0.02 sec)

mysql> use sec_test
Database changed
mysql> create table test (username char(16));
Query OK, 0 rows affected (0.01 sec)

ご覧の通り、char(16)でusernameというカラムを持つテーブルを作成します。

まず「admin」というusernameを持つ行を作ります。

mysql> insert into test (username) values ('admin');
Query OK, 1 row affected (0.00 sec)

mysql> select count(*) from test where username = 'admin';
+----------+
| count(*) |
+----------+
|        1 |
+----------+
1 row in set (0.00 sec)

出来ています。

次に、問い合わせの文字列部分の後ろに空白を追加してみます。全部で16文字になるようにしました。

mysql> select count(*) from test where username = 'admin           ';
+----------+
| count(*) |
+----------+
|        1 |
+----------+
1 row in set (0.00 sec)

後ろの空白が無視されているのがわかります。update:正確にいえば、char型なので指定された長さ未満の文字列は空白文字で埋められるみたいです。null文字とかじゃないんですね。

次に、char(16)の範囲を超えたところで「x」を追加して検索してみます。

mysql> select count(*) from test where username = 'admin           x';
+----------+
| count(*) |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec)

さすがにヒットしませんでしたが、そもそもカラムの制限長を超えているのにエラーにはなりません。

では、この値をINSERTしてみます。

mysql> insert into test (username) values ('admin           x');
Query OK, 1 row affected, 1 warning (0.01 sec)

mysql> select count(*) from test where username = 'admin';
+----------+
| count(*) |
+----------+
|        2 |
+----------+
1 row in set (0.01 sec)

「admin」としてINSERT出来てしまいました

これで、(1)既存のデータに重複行があるかチェック(2)なければINSERT、という処理で重複登録を防ごうとしてもダメなことがわかります。

ちなみにPostgreSQL 7.4.19でテストすると

db_test=# create table security_test (username char(16));
CREATE TABLE
db_test=# INSERT INTO security_test (username) VALUES ('admin');
INSERT 0 1
db_test=# SELECT COUNT(*) FROM security_test WHERE username = 'admin';
 count 
-------
     1
(1 row)

db_test=# SELECT COUNT(*) FROM security_test WHERE username = 'admin           ';
 count 
-------
     1
(1 row)

db_test=# SELECT COUNT(*) FROM security_test WHERE username = 'admin           x';
 count 
-------
     0
(1 row)

db_test=# SELECT COUNT(*) FROM security_test WHERE username = 'admin            ';
 count 
-------
     1
(1 row)

db_test=# INSERT INTO security_test (username) VALUES ('admin           x');
ERROR:  value too long for type character(16)
db_test=# SELECT COUNT(*) FROM security_test WHERE username = 'admin';
 count 
-------
     1
(1 row)

db_test=# 

INSERTはできません。8.3.3でも。

db_test=# create table test (username char(16));
CREATE TABLE
db_test=# INSERT INTO test (username) VALUES ('admin');
INSERT 0 1
db_test=# SELECT count(*) FROM test WHERE username = 'admin';
 count 
-------
     1
(1 row)

db_test=# SELECT count(*) FROM test WHERE username = 'admin           ';
 count 
-------
     1
(1 row)

db_test=# SELECT count(*) FROM test WHERE username = 'admin           x';
 count 
-------
     0
(1 row)

db_test=# SELECT count(*) FROM test WHERE username = 'admin            ';
 count 
-------
     1
(1 row)

db_test=# INSERT INTO test (username) VALUES ('admin           x');
ERROR:  value too long for type character(16)

INSERT時にあふれた分が勝手にカットされることはありませんでした。