Oct - 6th
HEADリクエストとLocationの小ネタ
Posted at 8:16 pm | Filed Under PHP, mobile
PHPでLocationヘッダを出力してリダイレクト処理をするとき、
<?php
header("Location: http://www.example.com/");
exit;
?>
こんな感じでリクエストを普通にリダイレクトで飛ばすのが大半なわけだが、このリクエストがHEADで来た場合、リダイレクト先へのリクエストも当然ながらHEADになってしまう。
携帯サイトでOBJECTタグを使った場合、auなどはダウンロードページのOBJECTタグのURLに記述された先へHEADリクエストが飛ばされる(ことがある)。そこでOKが返らないとちゃんと動作しない。いつも飛ぶわけではなさそうなので、キャッシュの問題もあるのかもしれないが再現性があんまりなくて深追いしていない。
で、たとえばセッションで認証していたりする場合、$_SERVERのREQUEST_METHODを参照してHEADリクエストの場合は別処理にしてやるなどしないと、OBJECTタグでアクセスする先のファイルで認証エラーが発生したらログインページにリダイレクトで飛ばそうなんて処理を入れたら、HEADリクエストのままリダイレクトされてしまうので正常に動作しない。
しかし、それはそれでHEADリクエストを使って認証をかいくぐる方法なんかが考案されたらまずい。とはいえ、
$_SERVER['REQUEST_METHOD'] = 'GET';
とかやってももちろんLocationヘッダでリダイレクトしたらHEADリクエストのまま飛んでいってしまう。
というわけで、OBJECTタグでアクセスする先のファイルで認証を入れたい場合は、失敗したらいっそ404を返してファイルが存在しない、とかしてやった方がいい。少なくともHEADリクエストのままどこかに飛んでいってページが正常に表示されなくなるよりはまし。
Sep - 12th
MySQLのカラム複製脆弱性
Posted at 3:45 pm | Filed Under MySQL, PHP, PostgreSQL, Security, WordPress
昨日書いた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時にあふれた分が勝手にカットされることはありませんでした。
Sep - 10th
WordPress 2.6.2はセキュリティ修正
Posted at 6:06 pm | Filed Under PHP, Security, WordPress
WordPress2.6.2が出たのでさっそく更新したが、今回のリリースにはセキュリティ関連の修正が入っていた。
PHPのrand関数はlibcのrandのラッパで、mt_randはメルセンヌ・ツイスタ擬似乱数生成装置の実装なのだが、どちらも32ビット型符号なし整数をシードにしている。しかし、実装の問題により実際にはそれ以下の強度にしかならないそうだ。そのため、暗号化に際してこれらの関数を利用するのは不適切ということになる。それに対処したようだ。
MySQLではデフォルトでサーバとクライアント間で送信されるクエリとその応答の長さがmax_packet_sizeディレクティブ(っていうのかな?それはApacheだけ?)で1MBに制限されている。もし1MBを超えたクエリがクライアントから送信された場合、MySQLサーバ側でエラーとして処理してしまう。
たとえば、期限の切れたセッションデータをDBから削除するようなクエリがあり、意図的にこのクエリが長くなるようなデータをセッションに埋め込むことができれば、クエリはいつまでも実行されず、何かしら問題が起きることになる。
update: もっとすごい問題があった。
MySQLにこんなクエリを投げる。
SELECT * FROM user WHERE username = 'admin';
たとえば、ユーザ登録の際に登録名の重複チェックをしているとしよう。WordPressのように、そのサイトにはadminという名のユーザが必ず存在するとして考える。新規のユーザが登録時に「admin」という名前を使おうとすると、上のようなクエリでチェックが行われ、すでに登録されている名前であるとしてアプリケーションによって登録がはじかれる。まあ、そういう仕組みのウェブアプリがあるものと思ってもらいたい。
このusernameというカラムが、たとえばchar(16)だったとする。MySQLではSELECTによる比較時にクエリ内の文字列の後ろの空白は無視されるようになっている。「admin 」だと、
SELECT * FROM user WHERE username = 'admin ';
こうなるが、実際には最初のクエリと同じ扱いになる。
ところで、じゃあ「admin x」という名前を使おうとしたらどうなるだろう。ここでは意図的に名前をchar(16)から外れるようにした。数えにくいかもしれないが、17文字ある。
SELECT * FROM user WHERE username = 'admin x';
MySQLでは、比較の際にはカラムの型を無視してくれるので、上のSQLは何も返ってこない。つまり、新規登録可能なユーザ名として認識される。そこで、INSERET処理を実行すると、今度はカラムの型をみて余分なところはカットし、さらに後ろの空白を取り除くので、あれま、「admin」という名前でユーザ登録が完了してしまう。
これでadminアカウントの乗っ取りが完了する。ひええ。
もちろん、UNIQUE INDEXで対処することは可能だが、そうでないアプリケーションなら困ったことになる。
未検証なのでなんともいえないが、本当だったらこわい。
update:再現しますた。
Sep - 8th
PHPのsplit関数
Posted at 10:07 pm | Filed Under PHP
Ruby、Perl、Pythonのsplit関数の挙動をまとめたページをつらつらと読んでいたのだが、そういえばPHPはどうなっているか気になったのでちょっと試してみた。
まずPHPのsplit関数だが、マニュアルによれば
説明
array split ( string $pattern , string $string [, int $limit ] )string を、正規表現によって配列に分割します。
となっている。つまり、splitの第一引数は正規表現しかない。
$str = "a,b,c";
print_r(split(",", $str));
/*当然下のようになる。
Array
(
[0] => a
[1] => b
[2] => c
)
*/
$str = "a,,c,,";
print_r(split(",", $str));
/* 末尾の空要素は無視されない。
Array
(
[0] => a
[1] =>
[2] => c
[3] =>
[4] =>
)
*/
$str = "";
print_r(split(",", $str));
/* 空文字を分割すると空の配列が返る
Array
(
[0] =>
)
*/
$str = "";
print_r(split("", $str));
/*
分割パターンが空だとエラーになって実行できない。
*/
$str = "a:b;c.d";
print_r(split("\.", $str));
/* 文字列では指定できないので意地になってみる。
Array
(
[0] => a:b;c
[1] => d
)
*/
$str = "a:b;c.d";
print_r(split("[:;.]", $str));
/* 正規表現だから範囲指定で。
Array
(
[0] => a
[1] => b
[2] => c
[3] => d
)
*/
$str = "a.b.c";
print_r(split("[.]", $str));
/* これも同じ。
Array
(
[0] => a
[1] => b
[2] => c
)
*/
$str = "a;b;c";
print_r(split(":", $str));
/* どれにもマッチしない。
Array
(
[0] => a;b;c
)
*/
と、まあここまではいい。正規表現なのか文字列なのか区別がないので、正規表現しか受け付けないという潔さも、まあいいだろう。文字列が使いたいならexplodeすればいいじゃない、と王妃もおっしゃってましたし。
ところが、納得できないのはこれだ。
$str = "a.b.c";
print_r(split(".", $str));
/* 全部空文字で返ってきたよ。。。
Array
(
[0] =>
[1] =>
[2] =>
[3] =>
[4] =>
[5] =>
)
*/
$ret = split(".", $str);
var_dump($ret);
/* 文字列だって言い張るんだよ。。。
array(6) {
[0]=>
string(0) ""
[1]=>
string(0) ""
[2]=>
string(0) ""
[3]=>
string(0) ""
[4]=>
string(0) ""
[5]=>
string(0) ""
}
*/
直感的には、空の配列かFALSEが返ってくるような気がするのだが、こういうことらしい。ううむ。
Aug - 27th
PHPのdefined関数でありがちなこと
Posted at 5:28 pm | Filed Under PHP
<?php
define(_A_CONSTANT, 'this is a constant');
if(defined(_A_CONSTANT)){
print("Hello");
}
?>
超特殊な場合を除き、絶対に「Hello」は出力されない。なぜなら、ちゃんとマニュアルにも記載されているが、上のコードは
<?php
define(_A_CONSTANT, 'this is a constant');
if(defined('this is a constant')){
print("Hello");
}
?>
と等価になってしまう。
でも他人のコードをデバッグしているときは、これはなかなか見つからない。
<?php
define(_A_CONSTANT, 'this is a constant');
if(defined('_A_CONSTANT')){
print("Hello");
}
?>
もちろん、これなら動く。
このパターンのバグが今日、職場で発見されたのでメモ。
Aug - 11th
Net_POP3でgmail
Posted at 10:25 am | Filed Under Family, Google, PHP
PEARのNet_POP3でGmailからメールを受信するのはこんな感じでできる。実行環境で「ssl://」で始まるデータストリーム型がサポートされていれば問題ない。調べるには
$ php -i | grep "Stream Socket" Registered Stream Socket Transports => tcp, udp, unix, udg, ssl, sslv3, sslv2, tls
このようにsslが表示されていればいい。
スクリプトは、例えば
<?php
require_once 'Net/POP3.php';
$host = 'ssl://pop.gmail.com';
$port = '995';
$user = 'Gmailのメールアドレス';
$pass = 'パスワード';
$pop = new Net_POP3();
$pop->connect($host, $port);
$pop->login($user, $pass, USER);
//成功したら配列が、失敗したらfalseが返る
$messages = $pop->getListing();
if($messages){
foreach($messages as $k => $row){
$id = $row['msg_id'];
//特にやることもないのでとりあえず配列に入れる
$mail[] = $pop->getMsg($id);
}
}
$pop->disconnect();
print_r($mail);
exit;
?>
こんな風になる。
今さらなんだよ、という向きもあるかもしれないが、例えばGoogleのメールサーバを使って独自ドメインサービスをやっている場合、これで自分のサーバで空メールのサービスを立ち上げるときなどに使えるので、まあメモということで。
Jul - 17th
PHPのセキュリティ修正
Posted at 4:59 pm | Filed Under PHP, Security
今朝デスクトップのCentOSでPHPのパッケージがPHP-5.1.6-20.el5_2.1に更新されていたので調べたら、RedHatからセキュリティ関連の更新がかかっていた。
解消されたのは以下の問題:
escapeshellcmdとhtmlspecialchars、htmlentities関数がマルチバイト文字列を正しく扱うことができず、クォート処理を回避されてしまう問題
add_rewrite_var()やsession.use_trans_sidを利用してリンク文字列やフォームに自動的にセッションIDを付与している場合、「ACTION」を含むフォームで外部のウェブサイトを飛び先に指定しているのにセッションIDを付与してしまう問題
fnmatch関数(聞いたことがなかった)で引数の文字数制限がないためオーバーフローを引き起こされる問題
randやmt_rand関数で生成される乱数が予測可能になっていた問題
とのこと。
しかし、BugtraqをみるとPHP-5.3.2以前のバージョンにある不具合だったりするのに、RHEL4のパッチが出ていないのがよくわからない。4.3.9系のパッケージが入っていたような気がするのだが。
May - 28th
Yahoo Japan APIの文章校正を試す
Posted at 11:14 pm | Filed Under PHP, Web Services
というわけで、先日遊んだ形態素解析に引き続き、今度はYahoo Japanの校正支援を使ったデモを作って遊んでみた。APIの詳細はこちら。
まだServices_Yahoo_JPには校正支援用の機能は実装されていないようだったので、形態素解析の中を見てちょろっと変えたらすぐに動いた。問題ないようなら後で投稿しておこう。
単純なAPIなので、filterを指定してあとはレスポンスを待つだけ。
<?php
require_once 'Services/Yahoo/JP/V1.php';
try{
$yahoo = Services_Yahoo_JP_V1::factory('kousei');
$yahoo->withAppID($app_id);
$yahoo->setSentence($keyword);
$yahoo->setFilter('1,2,3');
$result = $yahoo->submit();
} catch(Services_Yahoo_Exception $e){
$error = '接続に失敗したような気がします。';
}
$xml =& $result->xml;
if(count($xml)){
foreach($xml as $k => $row){
print_r($row);
}
}else{
print('正しい日本語みたいです。');
}
?>
こんな感じで実装できる。あんまりYahoo側の仕様がわかっていないから、とりあえず動いたところで放置している。
May - 27th
Microsoftからの刺客、その名もWIMP
Posted at 3:09 pm | Filed Under Apache, Linux, PHP, Windows
LAMPに対抗して、MicrosoftがIIS上でPHPを動かす環境を宣伝している。MSSQLではなくMySQLやPostgreSQLを宣伝するというのも愉快ではあるが。
それより何より、やはり気になるのは
Linux + Apache + MySQL + Perl(PHP, Python…) = LAMP
に対抗したキャンペーンの略称が、
Windows + IIS + MySQL + PHP = WIMP
になってしまっていることだろうか。
wimp
【名】弱虫、作話症{さくわしょう}、意気地なし、怖がり、勇気のないやつ、すぐあきらめるやつ
・You’re such a wimp. : 気が小さいね。
・That tequilla shot has been sitting there an hour, are you too much of a wimp to drink it?
さすが世界のMicrosoftはユーモアを忘れていない。
May - 22nd
Yahoo JapanのAPI、まめな病人
Posted at 1:04 pm | Filed Under Apache, PHP, Web Services
ブスコバンのアレルギーらしく、健康診断が終わってからずっとひどい頭痛だ。今日はちょっと家で休んでいる。
Yahoo JapanのAPI用に既存のPEARパッケージを拡張したServices_Yahoo_JPがある(作者サイト)。面白そうなのでさっそく使ってみた。
テキスト解析はこんな風に使える。例えばテキストの読みがなを取得する。
<?php
require_once 'Services/Yahoo/JP/MA.php';
$keyword = 'これは日本語です。';
$app_id = 'Your APP ID';
try{
$yahoo = Services_Yahoo_JP_MA::factory('parse');
$yahoo->withAppID($app_id);
$yahoo->setSentence($keyword);
$result = $yahoo->submit();
} catch(Services_Yahoo_Exception $e){
$error = 'すいません、YahooJapanに接続できませんでした。';
}
if($result){
$xml =& $result->xml->ma_result->word_list->word;
foreach($xml as $key => $val){
print($val->reading . "\n");
}
}
?>
なるほど。で、何か面白いことでもできないかと考えたが、Yahooから戻ってくる読みがなをさらにローマ字に変換して、Mac OS Xに付属のsayコマンドで読み上げてみるのはどうだろう。
$ say -o 出力ファイル "読み上げるテキスト"
上の書式で変換できるので、フロントエンドのPHPと連携してみれば簡単だ。
そこでふと思いついた。sayコマンドは声の指定も可能なのだが、プリセットされたデータには歌いながらしゃべるパターンもあったはずだ。それを指定すれば、入力したテキストを歌うこともできるじゃないか。
$ say -o 出力ファイル -v "声のパターン名" "読み上げるテキスト"
そうと決まれば話は早い。というわけでデモを作成した。(Proof of conceptということで無茶なことはしないでくださいね)
テキストが入力されると、バックエンドのMacBookにHTTPで通信してプロシージャを呼び出し、MacBook側はテキストを元に音声ファイルを作成してMP3に変換、最終的に作られたファイルをDocmentRoot以下に設置してファイルのパスを返す。フロントエンドのウェブサーバにリバースプロキシでMacBookのローカルファイルにアクセス可能にしてあげていっちょあがり。文字で書くとややこしいが、図にするといたって単純。

追記:Yahoo JapanのAPI側で数字の読み方は返してくれないみたいだ。