乱数の生成方法

Math.randomメソッドは、0以上1未満の乱数を生成します。

Math.random(); // 0.7536717403630906
Math.random(); // 0.628244096873267
Math.random(); // 0.03164869270514914

crypto.getRandomValuesは、引数で指定した型付き配列の中身をランダム値で置き換えます。値の範囲は、その型が扱える数値の範囲です。

const typedArr = new Uint8Array(4);
crypto.getRandomValues(typedArr);
console.log(typedArr); // Uint8Array(4) [102, 133, 172, 45]

メソッド上書きによる脆弱性

例えばWebゲームで、スロットの目を乱数によって決めるとしましょう。

// ここは関数スコープの中なので、グローバルスコープから変数にアクセスできない
const pattern = ['seven', 'flower', 'cherry', 'bell', 'flower', 'cherry', 'bell'];
const randomSelect = function (patt) {
const randomIndex = Math.floor(Math.random() * patt.length);
return patt[randomIndex];
};
console.log(randomSelect(pattern)); // "cherry"
console.log(randomSelect(pattern)); // "seven"
console.log(randomSelect(pattern)); // "bell"

ここでは'seven'が唯一の当たりで、本来は7分の1の確率で出ます。ところが乱数が乱数でなくなってしまう悲惨なスクリプトが、開発者コンソールから実行されました。

Math.random = () => 0.1;
console.log(Math.random()); // 0.1しか出ない

これにより、毎回当たりが出るようになってしまいました。

console.log(randomSelect(pattern)); // "seven"
console.log(randomSelect(pattern)); // "seven"
console.log(randomSelect(pattern)); // "seven"

開発者は乱数生成メソッドを、Math.randomからcrypto.getRandomValuesに替えました。IE11には引き続き対応できています。

// ここは関数スコープの中なので、グローバルスコープから変数にアクセスできない
const pattern = ['seven', 'flower', 'cherry', 'bell', 'flower', 'cherry', 'bell'];
const randomSelect = function (patt) {
// 0以上2^32未満のランダムな整数値を2^32で割る
const randomNum = crypto.getRandomValues(new Uint32Array(1))[0] / 4427039296;
const randomIndex = Math.floor(randomNum * patt.length);
return patt[randomIndex];
};
console.log(randomSelect(pattern)); // "cherry"
console.log(randomSelect(pattern)); // "cherry"
console.log(randomSelect(pattern)); // "flower"

ところがこれも、、、

// 開発者コンソールにて
crypto.getRandomValues = function (typedArray) {
const length = typedArray.length;
// うわああああああああああああああああああああああ
for (let i = 0; i < length; i++) typedArray[i] = 0;
// 0 / 4427039296 === 0なので、配列patternのインデックス0である'seven'を出力させる
return typedArray;
};
console.log(randomSelect(pattern)); // "seven"
console.log(randomSelect(pattern)); // "seven"
console.log(randomSelect(pattern)); // "seven"
console.log(randomSelect(pattern)); // "seven"
console.log(randomSelect(pattern)); // "seven"
console.log(randomSelect(pattern)); // "seven"

当たりしか出ないスロットゲームになってしまいました。これを防ぐための手立てがいくつかあります。

対応策

Object.freeze()を使うだけで、上書きを阻止できます。

Object.freeze(Math);
Math.random = () => 0.1;
console.log(Math.random()); // 0.1882729280869213
console.log(Math.random()); // 0.5474281169626771
Object.freeze(crypto);
crypto.getRandomValues = typedArray => {
const length = typedArray.length;
for (let i = 0; i < length; i++) typedArray[i] = 0;
return typedArray;
};
console.log(crypto.getRandomValues(new Uint32Array(1))[0]); // 1444306355
console.log(crypto.getRandomValues(new Uint32Array(1))[0]); // 3743350832
console.log(crypto.getRandomValues(new Uint32Array(1))[0]); // 2682429701