この記事は、 CakePHP Advent Calendar 2019 2日目の記事です。
1日目の記事は、hgsgtkさんの「来たるCakePHP 4.0 を知ろう – Qiita」でした。
TL;DR: PHP 5.6 を使っている人は paragonie/random_compat を入れましょう。
先月に、UUIDの衝突に関する話題が盛り上がりました。
10秒で衝突するUUIDの作り方 – Speaker Deck
この話のキモは、UUIDの生成に擬似乱数関数の mt_rand
を使用しているとシードにより乱数が固定化するので、同じUUIDが生成されてしまうということです。
特にプログラムコード中で不用意に mt_srand
を呼び出していると、衝突しやすくなります。
CakePHP にも Cake\Utility\Text::uuid
というメソッドがあり、UUIDv4が生成できるようになっています。データベーステーブルでidフィールドをchar(36)
で定義すると、レコード追加時にこのメソッドが呼ばれてUUIDがセットされるようになっています。
Cake\Utility\Text::uuid
は、
public static function uuid()
{
$random = function_exists('random_int') ? 'random_int' : 'mt_rand';
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
// 32 bits for "time_low"
$random(0, 65535),
$random(0, 65535),
// 16 bits for "time_mid"
$random(0, 65535),
// 12 bits before the 0100 of (version) 4 for "time_hi_and_version"
$random(0, 4095) | 0x4000,
// 16 bits, 8 bits for "clk_seq_hi_res",
// 8 bits for "clk_seq_low",
// two most significant bits holds zero and one for variant DCE1.1
$random(0, 0x3fff) | 0x8000,
// 48 bits for "node"
$random(0, 65535),
$random(0, 65535),
$random(0, 65535)
);
}
と実装されており、random_int
関数のない環境(< PHP 7.1)では、mt_rand関数が使用されます。
というわけで、Cake\Utility\Text::uuid()
を使用した 衝突のサンプルコード を書いてみました。
サンプルコード抜粋
use Cake\Utility\Text;
function make_seed()
{
list($usec, $sec) = explode(' ', microtime());
return $sec + $usec * 1000000;
}
$repeats = isset($argv[1]) ? (int)$argv[1] : 1;
for ($i = 0; $i < $repeats; $i++) {
mt_srand(make_seed()); // !! HAHAHAHAHHHAHA !!
echo Text::uuid() . "\n";
}
サンプルコードでは不用意な mt_srand
の使用例として、ありがちなマイクロ秒をシードとして与えています。
なお、シードとしてマイクロ秒を与えなくても mt_srand
が受け取るのは32bit整数の範囲なので、「10秒で衝突するUUIDの作り方」にあるように mt_srand
を呼び出している時点で65,536回試行すればだいたい衝突します。
この問題は、CakePHP 3.x を PHP5.6環境で利用している時に発生します。PHP 7.1以降では、random_int
関数が使用されるため mt_rand
に起因する問題は起こりません。
(CakePHP 4では、PHP 7.2以降のためこの問題は発生しません。)
Cake\Utility\Text::uuid()
の呼び出し前に、 mt_srand
が存在しない場合は衝突しにくくなりますが、分散環境でPID、時間が一致する場合は同じ乱数シードとなるので同じUUIDが生成されることがあります。
(参考: https://twitter.com/zeriyoshi/status/1199639847457046529)
というわけで、UUIDv4の生成にはmt_randを使わないようにする必要があります。そのためには、PHP 5.6でも random_int
等の関数を使うことができるようになる paragonie/random_compat パッケージを入れましょう。
以下のように、
composer require "paragonie/random_compat":"^2.0|9.99.99"
としてrandom_compatをインストールすれば、PHP7以降の環境では、何もしないバージョンである v9.99.99
が入りますので、将来的にPHPをバージョンアップする場合でも安心です。
補足
いちおう、この問題については Issue を上げていますので、将来のバージョン(3.9以降)では何らかの対策がされると思います。
Collision uuid in PHP 5.6 · Issue #13944 · cakephp/cakephp
Deprecation of mt_rand in uuid generation by nojimage · Pull Request #13958 · cakephp/cakephp
追記
3.9以降、paragonie/random_compatが必須となり、uuid生成でmt_randは使用されなくなります。
Deprecation of mt_rand in uuid generation by nojimage · Pull Request #13958 · cakephp/cakephp