Posts Tagged ‘cakephp’

CakePHP1.3.20、2.5.3で修正されたデータが全件更新されるやつを調べてみた

金曜日, 8月 8th, 2014

http://bakery.cakephp.org/articles/markstory/2014/07/21/cakephp_2_5_3_and_1_3_20_released

7/21に久々に1.3系を含むCakeのアップデートがリリースされました。
1.3系も更新されるやつは大体やばいやつ、ということで今回も何やらやばそうな事が書いてあります。

The 1.3.20 release contains an important fix to address a potential race condition in Model::save() that can cause data loss when records are deleted during concurrent updates.

data lossとはどういうことか、実際に試して確認してみました。

結論

save(update)処理の最中に対象レコードが削除されると、〜where1=1というえらいクエリが発行されて更新しようとした内容で全件updateが発生します。
速やかにバージョンアップしましょう。1.3系は他に変更点はなさそうです。

環境

PHP5.4.31
CakePHP1.3.19(再現環境)

試してみる

system_logsというテーブルに対してID指定で更新をかける下記のようなコードを実行。

$this->SystemLog->id = 100;
$updatedata = [];
$updatedata['SystemLog']['user_id'] = 1;
$this->SystemLog->save($updatedata);

id=100のレコードが存在する場合

下記のようなクエリが実行される。


SELECT COUNT(*) AS `count` FROM `system_logs` AS `SystemLog`   WHERE `SystemLog`.`id` = 100

SELECT COUNT(*) AS `count` FROM `system_logs` AS `SystemLog`   WHERE `SystemLog`.`id` = 100

UPDATE `system_logs` SET `user_id` = 1  WHERE `system_logs`.`id` = 100

カウントが二回実行されてますがどちらも存在チェック(model::exits())で呼ばれてます。
普通にアップデートされてますね。

id=100のレコードが存在しない場合

SELECT COUNT(*) AS `count` FROM `system_logs` AS `SystemLog`   WHERE `SystemLog`.`id` = 100

INSERT INTO `system_logs` (`user_id`, `created`) VALUES (1, '2014-08-07 19:00:29')

最初の存在チェックでレコードが存在しないため、insertが実行されました。

id=100のレコードが更新途中で削除された場合

初回の存在チェックと、二回目の存在チェック(DboSource::defaultConditions)より前にsleepを入れてレコード削除。

SELECT COUNT(*) AS `count` FROM `system_logs` AS `SystemLog`   WHERE `SystemLog`.`id` = 100

SELECT COUNT(*) AS `count` FROM `system_logs` AS `SystemLog`   WHERE `SystemLog`.`id` = 100

UPDATE `system_logs` SET `user_id` = 1  WHERE 1 = 1

WHERE1=1というやばそうなUPDATEが実行されました。

何が起きているのか?

ID指定ありで、Modell::save内の初回の存在チェックが通るとupdateとして処理が行われるのですが、その際のcondition(WHERE句)作成ロジック内で再度対象レコードの存在チェックが行われ、対象レコードが存在しないとdefaultCondition=nullとして扱われ、WHERE 1 = 1というSQLが発行されるようです。

dbo_source.phpの1877行あたり

   function defaultConditions(&$model, $conditions, $useAlias = true) {
        if (!empty($conditions)) {
            return $conditions;
        }
        $exists = $model->exists();
        if (!$exists && $conditions !== null) {
            return false;
        } elseif (!$exists) {
            return null; // 存在チェックに引っかかるとnullが返り、結果的にwhere1=1となる
        }
        $alias = $model->alias;

        if (!$useAlias) {
            $alias = $this->fullTableName($model, false);
        }
        return array("{$alias}.{$model->primaryKey}" => $model->getID()); //通常はこっち
    }

アップデート版の挙動

以下のように修正されてます。
$model->__safeUpdateModeはsaveによる更新時はtrueとなります。

    function defaultConditions(&$model, $conditions, $useAlias = true) {
        if (!empty($conditions)) {
            return $conditions;
        }
        $exists = $model->exists();
        if (!$exists && ($conditions !== null || !empty($model->__safeUpdateMode))) {
            return false;
        } elseif (!$exists) {
            return null;
        }
        $alias = $model->alias;

        if (!$useAlias) {
            $alias = $this->fullTableName($model, false);
        }
        return array("{$alias}.{$model->primaryKey}" => $model->getID());
    }

存在チェックに引っかかった場合はnullではなくfalseが返るようになります。
defaultConditionsでfalseが返された場合、実行されるSQLは以下のようになります。

UPDATE `system_logs` SET `user_id` = 1  WHERE 0 = 1

お、おう、という感じはしますが結果的に更新はかからなくなります。

影響は?

同一レコードに対する更新、削除が頻繁に行われるようなシステムでなければ(例えば特定のユーザーに紐付いたデータ等であれば)そんなに起こることはなさそうですが、バッチによる一斉更新とユーザー操作がかぶったりした場合に運悪いと遭遇しそうです。
速やかにアップデートしましょう。

最後に

nullとかfalseとかarrayとかが混在して戻ってくるやつをアレコレするの見てて辛い。


CakePHP1.3系でhttpsを経由するととセッションIDの再作成が出来ない問題の対応方法

金曜日, 11月 16th, 2012

問題点

CakePHP1.3系ではSecurity.levelをhighに設定するとセッション固定化攻撃対策のためのセッションIDの再作成(regenerate)が行われますが、その際にhttps→httpというページ遷移を行うとセッションが切れてログアウトしてしまいます。

理由はhttpsでアクセスされた場合、(Security.level等の設定に関係なく)CakeSessionクラス内でiniを書き換え自動的にsecure属性を付加しているからです。


// cake/libs/cake_session.php 475あたり
if ($iniSet && env('HTTPS')) {
    ini_set('session.cookie_secure', 1);
}

このため、regenerateされたタイミングで発行されたセッションクッキーは次にhttpのページが表示された際にサーバー側に送られず、セッションが切れてしまいます。

すべてのページがhttpsでアクセス出来るのであれば問題ないのですが、諸々の事情によりログイン画面等必要な部分のみhttpsでアクセスしたい場合上記の問題が発生します。

Security.levelをlowまたはmediumに設定すれば、httpアクセス時に発行されたセッションIDが使い続けられるので結果的にセッションは切れずに済むのですが、同じセッションIDが使い続けられるため、セッション固定化攻撃を受けた際のセキュリティリスクが高くなります。

対応方法

上述の挙動は全てcake/libs/cake_session.phpに書かれているため、コアクラスの挙動を上書きする必要があります。
とはいえ、コアクラスを書き換えるのは望ましくないため、こちらで記述されている方法を使ってapp以下に配置した同クラスで上書きします。

(参考)CakePHP フレームワークのソースをapp/で置き換える – Shin x blog

cake/libs/cake_session.phpをapp/libs/cake_session.phpにコピーした上で、app/config/bootstrap.phpに以下のコードを追加します。


App::import('Core', 'CakeSession', array('file' => APPLIBS .'cake_session.php'));

そして、コピーしたapp/libs/cake_session.phpを編集して以下の部分を修正します。


// 上述のsecure属性の付加をコメントアウト
//       if ($iniSet && env('HTTPS')) {
//           ini_set('session.cookie_secure', 1);
//      }

これにより、httpsで発行したセッション管理用クッキーがhttpでのアクセスでも送出されるようになり、regenerateを行いつつセッションを維持することが可能となります。
(もちろんhttpsで発行した別のクッキー情報もhttpで送られてしまうため、こちらは別途セキュリティを考慮する必要があります)

セッション用クッキーの生存期間

上述の問題とは関係ないのですが、Security.levelをhighに設定した場合、セッション用クッキーの生存期間は強制的に0(つまり同セッション内、通常はブラウザが閉じられるまで)に設定されます。

コメントには

‘high’   Session timeout in ‘Session.timeout’ x 10

と書かれているのですがソース上は下記のようになっています。


if ($this->security == 'high') {
    $this->cookieLifeTime = 0;
} else {
    $this->cookieLifeTime = Configure::read('Session.timeout') * (Security::inactiveMins() * 60);
}

セッションの保持期間はSession.timeout×セキュリティレベルに応じた値となってますが、highの場合は上述のコードにより、セッションクッキーの生存期間が強制的に同一セッション内のみに設定されます。

このあたりも要件に応じて変更するといいかもしれません。

まとめ

・cake1.3系(少なくとも1.3.6〜1.3.13までセッションまわりは同じ実装になっているのを確認しました)に於いては、httpsを部分的に使用したWebServiceの場合、regenerateと相性が悪いです。

・挙動を変更するためにはcake_sessionを上書きする必要があります。

・セッションクッキーを守るためには本来的にはセッションを使用する部分はすべてhttpsアクセスにしてセキュア属性をつけるべきですが、諸々の理由により一部分のみhttps実装になっている場合は上述のような対策を取る方がまだセキュアだと考えられます

cake1.3系に於いて「よくわからないけどとりあえずセキュリティレベルをmediumにしたら動いた」的なエントリを割と見かけるので一つのアプローチとして本エントリを書いてみました。
個人的にはクッキーやセッションに関する挙動はあまりフレームワークに上書きして欲しくないな、と思っているのですが、Cake2系ではこのあたりの挙動もだいぶ変わっているようなので必要があれば調べてみたいと思います。

最後に、セキュリティに関する問題はセンシティブなものだと考えておりますので、本エントリに関する間違いや違ったアプローチなど、是非突っ込みを頂ければと思います。


Windows+Eclipseで(ゴテゴテした)快適なCake開発環境を作る(CakePHP Advent Calendar 2011 11日目)

日曜日, 12月 11th, 2011

@mon_satさんからバトンを受け取ってのAdvent Calendar2011 11日目担当EG(@EGMC)です。
初参加で地味ネタですが、開発環境の話ってあまり聞かないのでとりあえず自分なりに育てた開発環境の話をしてみます。

※注 この前置きは長いのでさっさと本題に入りたい方は「開発環境構築編(ここから本編)」へどうぞ

ネタはWindows+EclipsePDT+その他tools。
昨今、勉強会に行くと皆MacBook Airなどを片手に、華麗にターミナルを開いてVimやEmacsで優雅にコーディングしている光景を目に致します。
そんな光景を横目に見つつ、しかしThinkpadでWindowsを起動してEclipseでもっさり開発している人たちも一定数いるはずだ!と信じてこのエントリーを書く次第です。いるかなあ。いるといいなあ。
前提として環境は

・開発サーバー(物理でもVPSでも)を外部(VMとかではなく)立てていて、DBは同サーバー(または同一ネットワーク内)にある
・ローカル環境はWindows+EclipsePDTで構築しており、プロジェクトは作成済み
・ソース編集はローカルで行うが、DBの操作やデバッグ(ログのtailとか)はリモートで行う
・ローカルのWindows環境にもphpがインストールされてる(bake用)

とします。
それ以前の情報は検索すればすぐ当たると思うので他に譲ります。
なお、cake1.3系で書きますが2.0系でも基本的に同じような事が出来ます。
環境設定上examle.netとかにしている部分は適宜ご自分の環境に置き換えてくださいませ。

なぜWindowsなのか?

・プラットフォームに依存したエディタとか、使い慣れたツールがたくさんあるからです。例えば私はHTMLエディタにCrecsent EVEを使ってます。

なぜEclipseなのか?

・元々自分がJava開発をメインでやってたのでその流れて使い始め、慣れていたというのが大きい
・起動の重さとかは色々あるものの、強力な補完機能(コード、Docコメント)、コードへのジャンプ、バージョン管理連携、プラグイン拡張など現実的な機能が多い
・最悪外部エディタが起動出来たり結構なんとかなる
・他の環境(Andoroid開発環境とか)と同居出来る
・起動の遅さについてはRAMDISKなどを使ってある程度は改善出来る

なぜ外にサーバーを立てるのか?

・自分の場合、会社と家で使うPCが違ったりするのでローカルに環境作ると何かと面倒くさい
・外部から叩かれる(例えばFacebookのプラグイン入れたりとか)ような機能を追加したりする
・他の人と環境を共有したりする
・↑みたいなことが無ければ全然VMでいいかなーと思ってます

使うツール群

Eclipse PDT(IDE)
WinSCP(ファイル転送)
PortFowarder(ポート転送)

開発環境構築編(ここから本編)

1)ファイルの自動転送

今回の構成では開発環境はリモートにあるのですが、ソースの反映は書き換えたらリアルタイムで行いたいですよね。
というわけで変更を即座に反映するよう自動転送します。
WinSCPの同期機能を使います。

ローカルはEclipseのプロジェクトフォルダを指定してパスを合わせます。
これで監視下のフォルダで変更があると即座にリモートへ転送してくれます。
無視したいファイル(.svnなど)はこんな感じで除外リストに入れておきましょう。

この状態でセッション保存しておけば、起動→接続→CTRL+Uですぐ同期をスタート出来ます。

2)Eclipseの自動補完を効かせる

なぜEclipseなのか?の所で補完機能を使いたいからだよ!と書きましたが、フレームワーク故にというかCakeはあまりモデルそのものをnewしたりしないので、そのままではほとんど補完が効きません(モデル、コンポーネント、etc・・)。
というわけで補完を効かせるようにコードをいじります。
正直、IDEの補完機能のために不要なコードを足すのはどうなの?という思いもありますが開発効率や動作速度を天秤にかけてこの方法を採用しています。

app_controller.phpに

/**
* Userモデル
*
* @var User
*/
var $User;

/**
* セッションコンポーネント
*
* @var SessionComponent
*/
var $Session;

/**
* authコンポーネント
*
* @var AuthComponent
*/
var $Auth;

/**
* セキュリティコンポーネント
*
* @var AuthComponent
*/
var $Security;

のように書いてどのクラスかを教えてあげます。

これで補完が効くようになります。

3)ローカルからbakeを使う

cakeといえばbakeですよね。
モデルやコントローラの生成が楽に行えますが、リモートで生成したファイルをいちいち持ってくるのは面倒です。
ローカルで(かつIDE内で)作ります。

bakeは当然phpが必要なのでローカルのWindowsマシンにphpを入れます。
bake.batはphpにパスが通ってる前提なのでパスを通します。

さて、これでbakeは実行出来るのですが、モデルの情報を取得するのにDBに接続しなければなりません。
ローカルにDB作るという方法もありますが、いちいち二重化させるのは面倒なのでリモートのDBに繋ぎたいです。
ポート転送を使いましょう。
Teratermのポート転送機能などもありますが、今回はPortForwarderを使います。

config.txtはこんな感じで

Host dev.example.net
HostName dev.example.net
# hostname or IP address

User hoge
# username on server. NEEDED

LocalForward 13306 localhost:3306
# localforward info1

Compression yes
# if you need compression

IdentityFile dev.example.net-key.rsa

13306ポートをリモートサーバーの3306に転送してます。
これでPortForwarderを立ち上げてDBの接続先を13306にすれば繋がります。
開発環境と、ローカルで接続情報が異なる場合は実行パスの判定などを行って接続先を切り替えます。

core.phpで切り替える場合は例えばこんな感じで

if (__FILE__ === 'C:\path_to_workspace\project\app\core.php') {
// ローカルでbakeするための設定
    define( '_DB_HOST' , '127.0.0.1:13306' );
    define( '_DB_NAME' , 'projectname' );
    define( '_DB_USER' , 'user' );
    define( '_DB_PASS' , 'pass' );
} else {
    // サーバーで動かすための設定
    define( '_DB_HOST' , '127.0.0.1:3306' );
    define( '_DB_NAME' , 'projectname' );
    define( '_DB_USER' , 'user' );
    define( '_DB_PASS' , 'pass' );
}

※ちなみにCake2.x系の場合はPORTは別指定になりますね

bake用の実行構成を実行→外部ツールから作成します。

これでIDEからbakeが実行出来ます。

ファイルの生成を行った後、F5でファイルのリフレッシュを行うと反映されますね。

ここまでやると晴れて快適に補完も効き、bakeも使え、ソースも同期される開発環境が構築出来ます。

ちなみにここまでの設定をすべて行って必要な諸々を起動すると画面はこんな感じになります。

・・・・まあ非常にWindowsらしい感じですね、うんうん。
立ち上げているものも多く、そもそもサーバー側で作業すれば楽なことも多々ありますが、まあこれはこれで慣れればいいものですよー。
Windowsで開発をされている方のお役に立てれば幸いです。
あと、こんな面倒なことしなくてもこんなんありまっせ!みたいな情報もあれば是非twitterなどで突っ込んで頂ければと思います。

そんなこんなで初参加のCakePHP Advent Calendar 2011でした。明日は@slumbers99さんです!