T

register_globals?そんなのデフォルトでオフですよ

Ruby on Railsのmass assignment絡みの脆弱性について、すごくよくまとまった記事。Railsで開発している人は必見ですね。

かつてPHPにはregister_globalsというこれと似た機能があり、現在はデフォルトでオフかつ5.4以降は廃止されたのですが、それまでは散々恐ろしい問題を引き起こしていました。PHPのウェブアプリケーションにセキュリティ問題が多いという評判が定着してしまったのは、これが主な原因といっても過言ではありません。PHPは何年もかけてようやく負の遺産を返済することができたのですが、Railsにはこのregister_globals相当の機能があり、今回見つかった脆弱性はかつてPHPが経験したことをそのままなぞっているようで興味深いところです。

ちなみに、Railsの本と名乗っていながら冒頭からRubyのオブジェクトの仕組みからメタプログラミングについて詳細に解説しまくる名著『実践Rails』では(初版なら)142ページ目にこの問題についての解説があります(Safari Onlineはこちら)。さすがです。

というわけで、何年も前に指摘されていた脆弱性に今頃慌てふためいている愚かでコンピュータサイエンスを学ぶには無能すぎてRailsくらいしか使えないくせにPHPerだなんだと他人の尻馬に乗って調子こいてたボケナス共は、土下座してmodelの修正に取りかかるがよい。わっはっは。

ありがちなforeachと参照渡し

ありがちなPHPの私。

$array = array('Ringo', 'George', 'John', 'Paul');
$new_array = array();
foreach($array as $k => $v){
  $new_array[] =& $v;
}
print_r($new_array);

もちろん、結果は

Array
(
    [0] => Paul
    [1] => Paul
    [2] => Paul
    [3] => Paul
)

HTTP通信のきほんてきなおはなし

サーバ間通信にHTTPを使うことはそんなに珍しくないので、何度も実装したことはあるのだけれど、その際にHTTPのバージョン指定に注意した方がいいよ、という話は見かけないのでちょっと書いておく。

HTTP通信のレイヤ7のやり取り、というとなんだか難しいので、よく実装するようなアプリケーションから見たHTTP通信のリクエストの中身は、まあだいたいこんな感じになっている:

GET /users/1 HTTP/1.1
Host: example.com

相手方にこれを送ってレスポンスを得るなら、愚直にPHPで実装すると

$host    = 'example.com';
$port    = 80;
$timeout = 30;
$content = '';
$request = "GET /users/1 HTTP/1.1\r\n";
$request .= "Host:" . $host . "\r\n";
$request .= "Connection: Close\r\n";
$request .= "\r\n";

$fp = @fsockopen($host, $port, $error, $errorno, $timeout);
if($fp){
  fputs($fp, $request);
  do{
    $data = fgets($fp);
    if(strlen($data) == 0){break;}
    $content .= $data;
  }while(true);
  fclose($fp);
}else{
  //なんかエラー処理
}

てなもんだろう。

でも、このコードにはとっても問題のある間違いが含まれている。たぶん、実行すると、通信相手先の状況にもよるが、一番困るのはループから出てこなくて制御が戻ってこない。

そこで、あれやこれやとデバッグが始まるのだが、User-Agentを指定してみたり、Content-Lengthを追加してみたり、悪戦苦闘の果てに、さんざんGoogleで調べて、遂に問題になっている箇所を(2つ)発見することになる。すなわち、

$request = "GET /users/1 HTTP/1.1\r\n";

ここを

$request = "GET /users/1 HTTP/1.0\r\n";

に直すか、

if(strlen($data) == 0){break;}

ここを

if(substr($data, -9) == "\r\n\r\n0\r\n\r\n"){break;}

にする。ようするに、何をやっているかというと、上の例ではHTTP/1.1で通信してしまうとKeep-Aliveが有効になってしまうので、相手方の仕様によっては通信したいやり取りが完了してもコネクションが切断されず、タイムアウトするまで制御が戻ってこない。そんな質の悪い環境を見つけたので今日こうしてこんなことを書いているわけですが、そこで1.0で通信することでKeep-Aliveを無効にしてしまうとうまくいった。泣けた。もうひとつの方は、単純にレスポンスの終わりを検知して強制的にループを抜けるようにしている。

と、まあ当たり前の話でしかないのだが、どこにもあんまり記述がないので、メモしておく。

MacBook Pro

MacBookのバッテリーが突然妊娠してしまい、バッテリーを外しても突然電源が落ちるようになってしまったので、思い切ってMacBook Proを購入した。奇妙なことに、MacBook Airの新型モデルが発売したその日の出来事である。

さすがに処理は体感できるレベルで速いし、特に文句もないのだが、持ち運び中に手元で開発できるようにApacheを設定していたらsegmentaion faultで落ちまくる。調べていて途中で面倒くさくなってgdbでプロセスをアタッチしてみたらよけいに面倒くさくなり困ったが、結局犯人は

(1)WebDAV関連モジュール

(2)PHPの設定ファイル

だった。(1)は使わないので単純にLoadModuleしないようにして、(2)は/etc/php.ini.defaultを/etc/php.iniに変更して保存した。妙なものだ。

変なCSRF対策

au端末のみアクセス可能なサイトでクロスサイトリクエストフォージェリー対策が必要になったのだが、ワンタイムトークンとかのよくある手段だとおもしろくないので、何かないかと考えてみた。

auの端末からのリクエストには、端末の契約情報に紐づいた一意のID(サブスクライバID)がリクエストヘッダとして送信される。そこで、こんなのを作ってみた。

(1)リモートアドレスでauのデートウェイからのリクエスト以外は弾く
(2)POSTリクエストを受け付けるURIはサブスクライバIDで異なる

まあ(1)はよくあることなのでいいとして、(2)はこんな感じになっている。公開するURLは「http://example.com/form」とかいう形式になっている。au端末からそのURLへのリクエストがあると、サーバ側で「https://example.com/xxxxxxxxxx/」にリダイレクトする。この「xxxxxxxxxx」はサブスクライバIDとサーバ側に保持している特定のsaltとなる文字列(例えば創世記の全文)から生成されたハッシュ値になっている。リクエストしてきた端末のサブスクライバIDから生成したハッシュ値とURIのハッシュ値が一致すればリクエストを通して、そうでなければ弾く。リダイレクト処理が入るので「http://example.com/form」へのPOSTリクエストはリクエストボディーの内容が消えたGETのリクエストになってしまうから、もし第三者がこのURLに対してCSRFを仕掛けてきても無効化される。ハッシュ値付きの正しいURLにPOSTリクエストを投げるようにしたくても、端末が送信するサブスクライバIDとsalt値とハッシュ値の生成ルールが分からなければ犠牲者の端末専用のURLが推測できないため攻撃用のフォームを作ることができない。

あとは.htaccessなどで存在しないディレクトリへのアクセスをリライトしてしまえばおしまい。

RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]

とかいう感じ。いやっほう。

あんまり見たことないやり方なので破り方があるかどうかちゃんと検証できてない気がするけど、思いつかないので実戦投入した。

携帯サイトのセキュリティ対策(ざっくり)

携帯サイト構築で忘れがちなセキュリティ対策を記録しておく。もちろん、外部に公開されているサーバで余計なポートが開放されていたり要らないサービスが起動していたりするのは論外。

SSL

SSLを利用している場合、基本的にSSLv3とTLS1以外を利用することはないので切っておく。特にNull暗号をオフにしておく必要がある。

Apacheだとこんな感じ。

SSLEngine on
SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:!LOW:!SSLv2:!EXP:!eNULL:!aNULL

Poundだとこんな感じ。

ListenHTTPS
  Ciphers "ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:!LOW:!SSLv2:!EXP:!eNULL:!aNULL"

Apache

リクエストヘッダのHost部分を書き換えられることを前提とした設定をしていないケースが多いと思われる。VirtualHostの先頭にデフォルトの設定を入れて対応する。mod_rewriteは入っているものとみなす。

Apacheのバージョンが低い場合のTRACEメソッドの禁止も追加。


  DocumentRoot /var/www/html
  ServerName default.host.name
  RewriteEngine on
  RewriteCond %{REQUEST_URI} !^/server-status
  RewriteRule .* - [F]
  #TRACEメソッドの禁止
  RewriteCond %{REQUEST_METHOD} ^TRACE
  RewriteRule .* - [F]

Aliasで指定されたiconsやmanualは不要なので削除。

#Alias /icons/ "/var/www/icons/"
#   
#
以下このディレクティブの最後まで。

PHP

基本的な設定はされているものとして、忘れがちなのをいくつか。php.iniの場合は

バージョン情報を晒さない。

expose_php = Off

リモートファイルをスクリプトとして読み込ませない。

allow_url_fopen = Off

リモートファイルをスクリプトとして読み込ませない。

display_errors = Off

続く、かも。

キャリア別インライン画像付きHTMLメールの正しい書式

仕事でいわゆるデコメール、アレンジメール、ようするに携帯向けHTMLメールを送信するプログラムを作成することになり、いろいろ悩んだので同じことになる人がいないようにメモしておく。

docomoには仕様書があるが他のキャリアにはなかったので、ほとんど勘で作業することになった。困ったポイントは(1)auのcidは@の数と書式に制限がある(2)SoftBankはboundary文字列の長さに変な制限があること。

HTMLメールに関しては、各キャリアともガラパゴスといわれるのも仕方がない仕様だと思う。外部の画像ファイルの読み込みを制限する必要があるのは理解できるが、それでもマルチパートのメールとしての書式くらいは守ってほしいものだ。

とりあえずいくつかの端末で動作したので公開しておく。今後不具合が見つかった場合に改修するかもしれない。

改行は全てCRLFとする。 【】内はフォーマットではなく フォーマットの詳細。

docomo

MIME-Version: 1.0
FROM: me@example.com
Subject: 【mime encodeしたJIS文字列】
TO: you@example.com
Content-Type: multipart/related; boundary="------------boundary1" 
Content-Transfer-Encoding: 7bit

--------------boundary1
Content-Type: multipart/alternative; boundary="------------boundary2" 
Content-Transfer-Encoding: 7bit

--------------boundary2
Content-Type: text/plain; charset=ISO-2022-JP
Content-Transfer-Encoding: 7bit

【mime encodeしたJIS文字列】

--------------boundary2
Content-Type: text/html; charset=ISO-2022-JP
Content-Transfer-Encoding: quoted-printable

【quoted printableにエンコードしたJISのHTML】
(画像は<IMG src="cid:0@0">のような書式になる)

--------------boundary2--

--------------boundary1 ←複数の画像がある場合はここから画像データまでを繰り返す
Content-Type: image/gif; name="【mime encodeしたJIS文字列】.gif" 
Content-Transfer-Encoding: base64
Content-ID: <0@0>

【base64_encodeしてchunk_splitした画像データ】

--------------boundary1--

au

MIME-Version: 1.0
FROM: me@example.com
Subject: 【mime encodeしたJIS文字列】
TO: you@example.com
Content-Type: multipart/mixed; boundary="------------boundary1" 

--------------15b49b545
Content-Type: multipart/alternative; boundary="------------boundary2" 

--------------boundary2
Content-Type: text/plain; charset=ISO-2022-JP
Content-Transfer-Encoding: 7bit

【mime encodeしたJIS文字列】

--------------boundary2
Content-Type: text/html; charset=ISO-2022-JP
Content-Transfer-Encoding: quoted-printable

【quoted printableにエンコードしたJISのHTML】
(画像は<IMG src="cid:0@0">のような書式になる)

--------------boundary2--

--------------boundary1 ←複数の画像がある場合はここから画像データまでを繰り返す
Content-Type: image/gif; name="【mime encodeしたJIS文字列】.gif" 
Content-Disposition: attachment; filename="【mime encodeしたJIS文字列】.gif" 
Content-Transfer-Encoding: base64
Content-ID: <0@0> ←必ず「数字@数字」とする

【base64_encodeしてchunk_splitした画像データ】

--------------boundary1--

SoftBank

MIME-Version: 1.0
FROM: me@example.com
Subject: 【mime encodeしたJIS文字列】
TO: you@example.com
Content-Type: multipart/related;
        boundary="------------boundary1" ←21文字にする必要がある
--------------boundary1
Content-Type: multipart/alternative;
        boundary="------------boundary2" ←21文字にする必要がある

--------------boundary2
Content-Type: text/plain; charset=ISO-2022-JP
Content-Transfer-Encoding: 7bit

【mime encodeしたJIS文字列】

--------------boundary2
Content-Type: text/html; charset=ISO-2022-JP
Content-Transfer-Encoding: quoted-printable

【quoted printableにエンコードしたJISのHTML】
(画像は<IMG src="cid:0@0">のような書式になる)

--------------boundary2--

--------------boundary1 ←複数の画像がある場合はここから画像データまでを繰り返す
Content-Type: image/gif; name="【mime encodeしたJIS文字列】.gif" 
Content-Disposition: inline;filename="【mime encodeしたJIS文字列】.gif" 
Content-Transfer-Encoding: base64
Content-ID: <0@0>

【base64_encodeしてchunk_splitした画像データ】

--------------boundary1--

途中PHPの関数名が出てきているが、気にしない。あと、PHPでquoted printableにするのにはimap_8bit関数を使った。

Xoopsわからんけどパッチ投げた

Xoops Cubeとやらをざっと眺めてみようと思ったら、手元の環境ではうんともすんとも動かない。

インストール中に「settings/site_default.ini.php」の中に「<?php」やコメントブロック「/*」などがあるので、このファイルをparse_ini_fileで読み込むとエラーになる。「themes/legacy_default/manifesto.ini.php」も同じ。

継承されたクラスから親クラスのコンストラクタを呼び出すときに、親クラスと同名の関数呼び出しをしているので、PHP5だとその名前の関数がなければ動かないしそもそもそれはコンストラクタではない。

宣言されていない変数を参照渡ししている箇所が山ほどある。

_MI_MESSAGE_DESCという定数が未定義のまま。

独自のエラーハンドラを持っているが、PHPで規定されているエラーを網羅してはおらず、それ以外は全て「Unknown Condition」となる。実害がないならいいが、参照渡しエラーが大量に出るので困る。

というわけで適当なパッチを投げた。いま考えたら最初の問題は別にキャッシュファイル消す必要はない気がする。

PHP-4.3.9でmemcache

PHP 4を使っている環境で5以上から対応のmemcachedがインストールできず、仕方なくmemcacheを使わなければいけない人は結構いるだろう。しかし、memcacheでもPHPのバージョンは4.3.11以上を要求されるので、ただでさえPHP5が使えないのにさらにより一層ハードに貧しい環境の人はkey valueストアを使おうとすると途端に困ってしまう。

しかし、実はPECLのmemcacheは4.3.9でもビルドして使うことができる。CentOS 4上で試したのだが、PECLのインストーラを使わずにソースコードをダウンロードして、

$ phpize

$ ./configure –disable-memcache-session

$ make

$ sudo make install

で問題なく動作する。同じ悩みを訴えていた人がいたので知った。

CentOS5でPHPのビルド中に出るエラー

「/usr/bin/ld: cannot find -lltdl」と出るときはyum install libtool-ldtl-devel。以上メモでした。