不十分な乱数の使用

セキュリティ用途での非暗号学的な乱数の使用

説明

再設定トークン、セッションID、OTP、CSRFトークンをMath.random()で作っても、予測に対する暗号学的な保証は得られません。実装や観測可能な出力によっては、後続の値を推測される可能性があります。値の範囲が小さい場合も総当たりが容易になります。セキュリティ用途では十分な長さと暗号学的に安全な乱数生成器が必要です。

想定される影響

  • トークンやセッションIDを推測され、不正ログインやセッションの乗っ取りにつながるおそれがあります。
  • 再設定トークンを推測され、パスワードを変更される可能性があります。
  • 予測可能なOTPによって追加の認証を回避されるおそれがあります。
  • 予測可能なCSRFトークンでは偽造リクエストを防げない可能性があります。
  • 鍵の予測やnonce・IVの要件違反によって、機密性や完全性が損なわれるおそれがあります。

対処方法

  • 暗号学的に安全な疑似乱数生成器(CSPRNG)を使ってください。ブラウザーのwindow.crypto.getRandomValues、Node.jsのcrypto.randomBytes、数値コード用のcrypto.randomIntなどがあります。
  • 推測を防ぐランダムトークンには、例えば128ビット以上の十分なエントロピーを確保してください。鍵、nonce、IVはアルゴリズムが求める長さ、一意性、予測困難性に従ってください。短いOTPには有効期限と試行回数の制限も必要です。
  • バイト列はhexやbase64urlでエンコードしてください。独自の文字集合へ変換する場合は、棄却法などで剰余演算による偏りを避けてください。
  • セキュリティ用途のパスワード、トークン、鍵、nonce、OTPにMath.random()を使わないでください。

例

変更前

javascript
// BAD: セキュリティ用途のトークンにMath.random()を使用
function createResetToken() {
  // 予測可能な出力でエントロピーも限定的
  const token =
    Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
  return token; // 再設定トークン、セッション、APIキーには不適切
}

変更後

javascript
// GOOD (Node.js): CSPRNGで256ビットのトークンを生成
const crypto = require("crypto");
function createResetToken() {
  return crypto.randomBytes(32).toString("hex"); // 64桁のhex。エンコードによる偏りなし
}

// GOOD (Browser): Web Crypto API
function createCsrfToken() {
  const bytes = crypto.getRandomValues(new Uint8Array(32)); // 256ビット
  // base64urlにエンコード
  let b64 = btoa(String.fromCharCode(...bytes))
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
  return b64;
}

Math.random()の出力を文字列に変えても、暗号学的な安全性は加わりません。変更後のNode.js部分とブラウザー部分は、それぞれの環境で別々に実行する例です。各APIで生成した32バイトをhexまたはbase64urlで表します。実際のトークンには用途、有効期限、使用回数の制限と、保存・転送時の保護も適用してください。

参考資料