はじめに
PHP8上級認定試験の模擬問題で勉強している際「CSRFトークンの生成」について、uniqid()やmt_rand()の使い方につい考える機会がありました。これらの関数は便利ですが、実際には「推測困難なトークン」には使えません。本記事では、その理由と正しいアプローチを整理しておきます。
目次
問題文のポイント
- CSRF等で必要な「推測困難なトークン」
- uniqid() を使う
- mt_rand() と組み合わせる
一見すると「ユニークで安全そう」に見えるが…
uniqid() の性質
PHPマニュアルに記載のシグネチャ:
uniqid(string $prefix = "", bool $more_entropy = false): string
- マイクロ秒単位の時刻を元にIDを生成
prefix
を指定すれば接頭辞を追加できる(より一意性を高めることができる)more_entropy = true
にすると小数点付きでより一意性が高まる- 暗号学的に安全ではない(マニュアルにも明記)
- 用途は「一意なID生成」であり「推測困難性の確保」ではない
- 現在時刻を元にしているため、複数のサーバでマイクロ秒まで同じ時刻で、uniqidを実行すると同じ値が生成されてしまうので注意。
画像やファイルのアップロード時で、アップロードファイルを保存する際のファイル名の生成などで利用。
mt_rand() の性質
PHPマニュアルに記載のシグネチャ:
mt_rand(int $min = 0, int $max = PHP_INT_MAX): int
- メルセンヌ・ツイスタを使った擬似乱数生成器
- 高速で品質は高いが暗号的には安全でない
- シードがわかれば次の出力が予測可能
- したがってセキュリティ用途には不適
ウェブサイトのID生成、シミュレーション、ゲームなど、予測困難な数値を生成する必要がある場面で広く利用。
問題のコード
var_dump(uniqid((string)mt_rand(), true));
実行できるし結果も変わるが、推測困難性は確保できない。
正しいアプローチ
$token = bin2hex(random_bytes(32)); // 64文字の推測困難トークン
これが CSRF トークン等で推奨される方法。
random_bytes(32)
が生み出すもの
- 32バイト = 256ビット のランダムデータ
- OS が提供する 暗号学的に安全な乱数ソース (CSPRNG) を利用
- キーボード入力、マウス動作、システムイベントなど、予測できないノイズを元に生成
bin2hex()
で文字列化
- 1バイト = 8ビット → 16進数で2桁表現
- 32バイト → 64文字の16進数文字列に変換
"9f2c1e3ab7c4d8...7be12f"
なぜ推測困難なのか
情報量が膨大
- 256ビットの組み合わせ数 = 2^256 ≒ 1.16 × 10^77
- 宇宙の原子の数よりも桁違いに多い
安全な乱数ソース
- uniqid() や mt_rand() のように「時刻」や「シード」に依存しない
- 外部から予測することは不可能
まとめ
- uniqid() は「一意性」用であり、「推測困難性」には不向き
- mt_rand() も暗号学的に安全ではない
- CSRF等には
random_bytes()
/random_int()
を使うのが正解