Posts Tagged ‘php’

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系ではこのあたりの挙動もだいぶ変わっているようなので必要があれば調べてみたいと思います。

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


FuelPHP 勉強会 東京 vol.2でOAuthログインの発表をしてきた

日曜日, 9月 23rd, 2012

LTしてきました。

とりあえず発表資料を置いておきます。

発表内容は実質fuel-ninjauthパッケージの紹介みたいになってるので、割とライトな内容かなと思ったのですが、思いの外fuel-ninjauthが知られていなかったのでまあ意味はあったのかな、と思います。

スライド後半にちょろっと書いてありますが、本当はここからSimpleAuthの独自ログインドライバーを作成してみて、なんかイマイチだったんで〜みたいな話をしたかったのですが時間の関係で割愛しました(まあ質問したかっただけで内容は特にないですけどね)

ログイン認証、セッション管理まわりは割と皆様使う領域(特にWebサービス作りたいみたいな人が多かったので)ではないかと思うので、今後とも情報が出てくるといいな、と思っております。

そのうち自分でユーザーログイン管理パッケージを作るかもしれませんが予定は未定です。

最後になりましたがスライドに間違い、質問等あれば是非Twitterでもなんでもいいので突っ込みください。