説明
暗号学的に安全な乱数を %(剰余)などで狭い範囲に変換すると、元の範囲を変換先の範囲で割り切れない場合に分布が偏ります。これをモジュロバイアスといい、一部の値が他の値より頻繁に現れます。攻撃者がこの偏りを利用すると、トークン、コード、識別子の推測が成功する確率が高まったり、値の重複を悪用されたりするおそれがあります。
想定される影響
- パスワード再設定トークン、セッション ID、OTP で頻出する値があると、推測の成功率が高まる場合があります。
- 取り得る値がなくなるとは限りませんが、頻出する値から試すことで成功確率を上げられる可能性があります。
- CSRF トークン、招待コード、登録コードを推測されると、アカウントの侵害や権限の悪用につながるおそれがあります。
- 認証コードやクーポンコードの重複によって、再利用、不正利用、サービスの悪用の危険が高まる場合があります。
対処方法
- 暗号学的乱数に
%などの縮小演算を直接適用しないでください。 - Node.js では、
crypto.randomInt()のように偏りを避ける API を使用してください。 - 範囲の縮小を自分で実装する必要がある場合は、棄却法(rejection sampling)で、均等に割り切れる範囲の外にある値を捨ててください。
- 文字列トークンには
crypto-random-stringなどの検証されたライブラリを使うか、十分な長さの乱数バイト列を hex や base64url などでそのままエンコードしてください。
例
変更前
javascript
// 危険: モジュロバイアスのある 6 桁のコードを生成
const crypto = require("crypto");
function generateOtpBad() {
// % で 0..999999 に範囲を縮めると偏りが生じる
const raw = crypto.randomBytes(4).readUInt32BE();
const n = raw % 1_000_000; // 危険: 一様分布ではない
return n.toString().padStart(6, "0");
}
console.log(generateOtpBad());
変更後
javascript
// 偏りを避ける API または棄却法を使用
const crypto = require("crypto");
// 1) 推奨: Node.js 組み込みの偏りを避ける API
function generateOtpSafe() {
const n = crypto.randomInt(0, 1_000_000); // 上限を含まない一様分布
return n.toString().padStart(6, "0");
}
// 2) 代替手段: 棄却法(rejection sampling)
function generateOtpSafeLegacy() {
const bound = 1_000_000;
const max = Math.floor(0x1_0000_0000 / bound) * bound; // 2^32 以下で最大の bound の倍数
while (true) {
const r = crypto.randomBytes(4).readUInt32BE();
if (r < max) {
return (r % bound).toString().padStart(6, "0");
}
}
}
console.log(generateOtpSafe());
説明:
- 変更前:
0..2^32-1の乱数を 1,000,000 で割った余りを使います。2^32 は 1,000,000 で割り切れないため、一部の値が多く現れ、攻撃者が特定のコードやトークンを当てる確率が高まります。 - 変更後:
crypto.randomInt()は偏りを避け、指定範囲で一様な分布を生成します。棄却法では、均等に割り切れる上限(max)以上の値を捨ててから剰余を取るため、各値の確率が等しくなります。