説明
トークン、セッションID、パスワード再設定コードなど、予測できてはならない値に math/rand を使うと、セキュリティに必要な乱数の保証を得られません。アルゴリズムとシードによっては、観測した値や推測したシードから後続の値を予測できるため、セキュリティ用途には crypto/rand が必要です。
想定される影響
- 予測可能なトークンやセッションIDで、アカウントやセッションを乗っ取られるおそれがあります。
- 再設定・認証コードを推測され、認証手順を迂回される可能性があります。
- 予測不可能性が必要な暗号パラメーターを誤って生成すると、機密性や完全性が弱まるおそれがあります。
対処方法
- セキュリティ用の乱数は
crypto/randで生成し、IVとnonceには使用するアルゴリズムの長さ・一意性の要件も適用してください。 math/randは統計的なサンプリングなど、予測可能性がセキュリティへ影響しない用途に限定してください。- 再設定トークンには例のように十分なエントロピーを確保し、有効期限、一度だけの使用、試行回数制限を組み合わせてください。
- 共通の生成関数を使い、乱数の長さとエンコードをレビューしてください。実際のトークンをログに残さないでください。
例
変更前の時刻によるシード設定は、Seed が有効な旧来の動作についての説明です。Go 1.24以降、パッケージの math/rand.Seed は既定では何も行いません。この変更で math/rand がセキュリティ用APIになるわけではありません。fmt.Println は結果の確認用であり、本番ログへトークンを出力しないでください。
変更前
go
package main
import (
"fmt"
mrand "math/rand"
"time"
)
// math/randで再設定トークンを生成する弱い例
func generateResetTokenWeak() string {
// 旧来の動作: 推測可能な時刻のシード(Go 1.24以降は既定で無効)
mrand.Seed(time.Now().UnixNano())
letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
b := make([]rune, 16) // 16文字のトークン
for i := range b {
b[i] = letters[mrand.Intn(len(letters))]
}
return string(b)
}
func main() {
token := generateResetTokenWeak()
fmt.Println("reset token:", token)
}
変更後
go
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
)
// crypto/randで再設定トークンを生成
func generateResetTokenSecure() (string, error) {
// 16バイト(128ビット)の乱数を32桁の16進数にする
b := make([]byte, 16)
// crypto/rand.Readで暗号学的に安全な乱数を生成
if _, err := rand.Read(b); err != nil {
return "", err
}
// バイト列を16進数の文字列で表す
return hex.EncodeToString(b), nil
}
func main() {
token, err := generateResetTokenSecure()
if err != nil {
panic(err)
}
fmt.Println("reset token:", token)
}
解説:
- 変更前: セキュリティ用ではない乱数APIで再設定トークンを生成します。時刻のシードが適用される環境では、推測できるシードが追加のリスクになります。
- 変更後:
crypto/randで16バイトの乱数を生成し、32桁の16進数で表します。エンコードによってエントロピーが増えるわけではありません。保存、有効期限、使用時の検証は別途実装する必要があります。