CWE-330: Use of Insufficiently Random Values - Go
Overview
Use of insufficiently random values in Go applications occurs when predictable or weak random number generation is used for security-sensitive operations, allowing attackers to predict or reproduce supposedly random values. Go provides two distinct random number generation packages: math/rand for pseudo-random numbers (PRNG) suitable for simulations and games, and crypto/rand for cryptographically secure random numbers (CSPRNG) required for security contexts. Using math/rand for security purposes is a critical vulnerability because its output is deterministic - given the same seed, it produces the same sequence of numbers.
Security-critical operations requiring cryptographically secure randomness include session token generation, password reset tokens, CSRF tokens, API keys, encryption keys and nonces, initialization vectors (IVs), salts for password hashing, and any identifier used for authentication or authorization. Predictable random values in these contexts enable session hijacking (guessing session IDs), account takeover (predicting reset tokens), CSRF attacks (reproducing CSRF tokens), and cryptographic breaks (repeating nonces in encryption).
The fundamental problem with math/rand is that it's seeded with a known or guessable value (often time.Now().UnixNano()), and its algorithm is designed for speed and uniform distribution, not unpredictability. Attackers who know or can guess the seed can reproduce the entire sequence of "random" values. Even with random seeding, math/rand has a limited state space (int64) that can be brute-forced. Time-based seeds are especially vulnerable - attackers know approximately when values were generated and can test a small range of possible seeds.
Primary Defence: Use crypto/rand.Reader with io.ReadFull() for all security-sensitive random value generation. Never use math/rand for authentication tokens, encryption keys, nonces, or any security-critical purpose. Ensure random values have sufficient entropy (128+ bits for tokens, 256 bits for encryption keys).
Common Vulnerable Patterns
math/rand for Session Tokens
// VULNERABLE - Predictable session token generation
package main
import (
"fmt"
"math/rand"
"time"
)
func init() {
// DANGEROUS: Seeding with time is predictable
rand.Seed(time.Now().UnixNano())
}
func generateSessionToken() string {
// VULNERABLE: math/rand is deterministic
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
token := make([]byte, 32)
for i := range token {
token[i] = charset[rand.Intn(len(charset))]
}
return string(token)
}
// ATTACK:
// Attacker knows session was created around 2024-02-06 14:30:00
// Tests seeds from time.Now().UnixNano() around that time
// Reproduces the PRNG sequence to predict session tokens
// Can hijack sessions by guessing valid tokens
Why this is vulnerable: math/rand is a linear congruential generator - entirely deterministic given the seed. Seeding with time.Now().UnixNano() seems random but provides only ~30 bits of entropy (limited by clock resolution and attacker's knowledge of approximate generation time). An attacker can brute-force seeds in the plausible time window (seconds or minutes), reproduce the PRNG state, and predict all tokens generated from that seed. For a server restarted at a known time, all session tokens become predictable.
Weak Password Reset Tokens
// VULNERABLE - Predictable reset tokens
import (
"fmt"
"math/rand"
)
func generateResetToken(userID int) string {
// DANGEROUS: Seeding with user data
rand.Seed(int64(userID + int(time.Now().Unix())))
// 6-digit code
code := rand.Intn(1000000)
return fmt.Sprintf("%06d", code)
}
// ATTACK:
// Attacker knows their user ID and approximate request time
// Computes possible seeds and corresponding codes
// Tests a small number of 6-digit codes to take over any account
Why this is vulnerable: A 6-digit code has only 1 million possible values - far too small for a security token. Seeding with userID + timestamp makes the seed highly predictable. Even without knowing the exact second, testing seeds for a 10-minute window with all user IDs (if sequential) is trivial. The attacker can generate the reset code for any user without receiving the reset email. This enables complete account takeover. Additionally, short numeric codes are vulnerable to brute force even without seed prediction.
Time-Based Seeds with Known Timing
// VULNERABLE - Time-seeded randomness for security
import (
"math/rand"
"time"
)
type Game struct {
secretValue int
}
func NewGame() *Game {
// VULNERABLE: Seeded when game starts
rand.Seed(time.Now().UnixNano())
return &Game{
secretValue: rand.Intn(1000000),
}
}
// Even for games, if timing is observable:
// Attacker connects, notes connection time
// Replicates seed from that time
// Predicts all future random events
Why this is vulnerable: When the seeding time is observable (server start, user login, game initialization), attackers can reproduce the seed. time.Now().UnixNano() has microsecond precision, but attackers can test all seeds within the possible time window. For a server that seeds at startup, the boot time might be logged or observable through monitoring. Once the seed is known, all subsequent math/rand outputs are perfectly predictable. In games, this allows cheating; in security contexts, it allows complete compromise.
Using math/rand for Encryption Nonces
// VULNERABLE - Predictable nonces break encryption
import (
"crypto/aes"
"crypto/cipher"
"math/rand"
)
func encryptData(key, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// DANGEROUS: math/rand for nonce generation
nonce := make([]byte, gcm.NonceSize())
for i := range nonce {
nonce[i] = byte(rand.Intn(256))
}
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
return append(nonce, ciphertext...), nil
}
// VULNERABILITY:
// Predictable nonces allow attackers to:
// 1. Reproduce nonces and verify ciphertext candidates
// 2. Reuse detection - same nonce with same key breaks encryption
// 3. Nonce prediction enables chosen-plaintext attacks on GCM
Why this is vulnerable: Encryption modes like GCM require unique, unpredictable nonces for each message encrypted with the same key. Predictable nonces completely break security: if attackers can predict or reproduce nonces, they can decrypt messages by brute-forcing the plaintext (especially for short messages or known formats). Nonce reuse is catastrophic in GCM - encrypting two different messages with the same key and nonce allows attackers to XOR the ciphertexts and recover plaintext. math/rand makes nonce prediction trivial if the seed is known.
Sequential or Guessable API Keys
// VULNERABLE - Sequential API key generation
import (
"fmt"
"sync"
)
var (
keyCounter int
counterMu sync.Mutex
)
func generateAPIKey() string {
counterMu.Lock()
keyCounter++
id := keyCounter
counterMu.Unlock()
// VULNERABLE: Sequential, predictable keys
return fmt.Sprintf("API-KEY-%08d", id)
}
// ATTACK:
// Attacker receives API-KEY-00012345
// Knows other valid keys are nearby: 00012344, 00012346, etc.
// Can enumerate and test all keys in range
Why this is vulnerable: Sequential IDs are completely predictable. An attacker with one valid API key can guess others by incrementing/decrementing the counter. Even with randomization, math/rand allows predicting the sequence. API keys must be cryptographically random with high entropy (128-256 bits) to prevent enumeration attacks. A guessable API key enables unauthorized API access, data theft, and service abuse. Some systems try obfuscation (base64 encoding counters), but this provides no real security.
Secure Patterns
Cryptographically Secure Session Tokens
// SECURE - Session tokens with crypto/rand
package main
import (
"crypto/rand"
"encoding/base64"
"io"
)
func generateSecureSessionToken() (string, error) {
// SECURE: 32 bytes = 256 bits of entropy
bytes := make([]byte, 32)
// SECURE: crypto/rand provides cryptographically secure randomness
if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
return "", err
}
// Encode to string (URL-safe base64)
return base64.URLEncoding.EncodeToString(bytes), nil
}
// Usage
func createSession(userID string) (string, error) {
token, err := generateSecureSessionToken()
if err != nil {
return "", err
}
// Store session in database/cache
storeSession(token, userID)
return token, nil
}
func storeSession(token, userID string) {
// Implementation: Redis, database, etc.
}
Why this works: crypto/rand.Reader is a cryptographically secure random number generator (CSPRNG) that reads from the operating system's entropy source (/dev/urandom on Unix, CryptGenRandom on Windows). It's designed to be unpredictable even to attackers who can observe some outputs. 32 bytes (256 bits) provides sufficient entropy to prevent brute-force attacks - there are 2^256 possible tokens, making random guessing infeasible. io.ReadFull ensures the entire buffer is filled with random data, failing if insufficient randomness is available (though in practice, modern OS entropy sources don't deplete). Base64 URL encoding makes tokens URL-safe for cookies and query parameters.
Secure Password Reset Tokens
// SECURE - Unpredictable reset tokens with expiration
import (
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"time"
)
type ResetToken struct {
Token string
UserID string
ExpiresAt time.Time
}
func generatePasswordResetToken(userID string) (*ResetToken, error) {
// SECURE: 32 bytes = 256-bit token
bytes := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
return nil, err
}
token := hex.EncodeToString(bytes)
resetToken := &ResetToken{
Token: token,
UserID: userID,
ExpiresAt: time.Now().Add(15 * time.Minute), // 15-minute expiry
}
// Store in database with expiry
storeResetToken(resetToken)
return resetToken, nil
}
func validateResetToken(token string) (string, error) {
// Retrieve from database
resetToken := getResetToken(token)
if resetToken == nil {
return "", fmt.Errorf("invalid token")
}
// Check expiration
if time.Now().After(resetToken.ExpiresAt) {
return "", fmt.Errorf("token expired")
}
// Delete token (single use)
deleteResetToken(token)
return resetToken.UserID, nil
}
func storeResetToken(token *ResetToken) {
// Implementation: Database with TTL
}
func getResetToken(token string) *ResetToken {
// Implementation: Database lookup
return nil
}
func deleteResetToken(token string) {
// Implementation: Delete from database
}
Why this works: 256-bit tokens provide 2^256 possible values, making brute-force attacks computationally infeasible. crypto/rand ensures each token is unpredictable and unique. Hex encoding (64 characters) makes tokens safe for URLs and emails. Time-based expiration limits the attack window - tokens are valid for only 15 minutes, reducing exposure. Single-use tokens (deleted after validation) prevent replay attacks. Storing tokens server-side with user association prevents token manipulation. This design prevents enumeration, brute force, and prediction attacks.
Secure API Key Generation
// SECURE - Cryptographically random API keys
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
)
type APIKey struct {
Key string // Public key given to user
Hash string // Hashed key stored in database
CreatedAt time.Time
}
func generateAPIKey() (*APIKey, error) {
// SECURE: 32 bytes of entropy
bytes := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
return nil, err
}
// Format: "sk_" prefix + base64 key
key := "sk_" + base64.URLEncoding.EncodeToString(bytes)
// SECURE: Hash for storage (don't store plaintext keys)
hash := sha256.Sum256([]byte(key))
hashStr := base64.URLEncoding.EncodeToString(hash[:])
return &APIKey{
Key: key,
Hash: hashStr,
CreatedAt: time.Now(),
}, nil
}
func validateAPIKey(providedKey string) (bool, error) {
// Hash the provided key
hash := sha256.Sum256([]byte(providedKey))
hashStr := base64.URLEncoding.EncodeToString(hash[:])
// Look up in database by hash
exists := checkAPIKeyHash(hashStr)
return exists, nil
}
func checkAPIKeyHash(hash string) bool {
// Implementation: Database lookup
return false
}
Why this works: API keys have 256 bits of entropy from crypto/rand, making enumeration impossible. The "sk_" prefix identifies the key type (secret key), helping detect accidental exposure in logs or repositories. Keys are hashed before database storage using SHA-256, so database compromise doesn't expose the actual keys (similar to password hashing). Validation requires hashing the provided key and comparing to stored hashes. Base64 URL encoding creates readable, URL-safe strings. This pattern is used by services like Stripe, OpenAI, and GitHub.
Secure Random Values for Cryptography
// SECURE - Random keys and nonces for AES-GCM
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"io"
)
func generateEncryptionKey() ([]byte, error) {
// SECURE: 32 bytes for AES-256
key := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
return nil, err
}
return key, nil
}
func encryptWithSecureNonce(key, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// SECURE: Cryptographically random nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
// Encrypt
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
return ciphertext, nil
}
Why this works: crypto/rand provides unpredictable, high-entropy random values suitable for cryptographic operations. Keys generated with crypto/rand are truly random, not reproducible by attackers. Nonces (number used once) must be unique and unpredictable for each encryption; crypto/rand ensures this. Using io.ReadFull guarantees the buffer is completely filled with random data or returns an error. This is the standard pattern for all cryptographic random value generation in Go.
Testing with Deterministic Randomness
// SECURE - Using math/rand safely in tests
// +build test
package main_test
import (
"math/rand"
"testing"
)
func TestRandomDataProcessing(t *testing.T) {
// SAFE: math/rand is fine for test data generation
// Not used for security, just test reproducibility
rand.Seed(42) // Fixed seed for reproducible tests
testData := make([]int, 100)
for i := range testData {
testData[i] = rand.Intn(1000)
}
// Test business logic with random data
result := processData(testData)
// Assert results
if result < 0 {
t.Errorf("Unexpected result: %d", result)
}
}
func processData(data []int) int {
// Business logic
return 0
}
// IMPORTANT: Never use math/rand in production security code
// This pattern is ONLY safe for testing, simulations, games
Why this works: math/rand is appropriate for test data generation where reproducibility is desired (fixed seed produces same test data) and security doesn't matter. Tests don't handle real user data or credentials. Using a fixed seed (42) makes tests deterministic - they produce the same results every run, which is essential for reliable testing. However, production code must never use math/rand for security. This demonstrates the correct separation: crypto/rand for security, math/rand for simulations and tests.
Secure Random Integers in Range
// SECURE - Unbiased random integers from crypto/rand
import (
"crypto/rand"
"encoding/binary"
"math/big"
)
// Method 1: Using crypto/rand with math/big (preferred for ranges)
func secureRandomInt(max int64) (int64, error) {
// SECURE: Cryptographically secure random in range [0, max)
n, err := rand.Int(rand.Reader, big.NewInt(max))
if err != nil {
return 0, err
}
return n.Int64(), nil
}
// Method 2: Generate random uint64
func secureRandomUint64() (uint64, error) {
var bytes [8]byte
if _, err := io.ReadFull(rand.Reader, bytes[:]); err != nil {
return 0, err
}
return binary.BigEndian.Uint64(bytes[:]), nil
}
// Usage example: Secure random selection from slice
func selectRandomItem(items []string) (string, error) {
if len(items) == 0 {
return "", fmt.Errorf("empty slice")
}
idx, err := secureRandomInt(int64(len(items)))
if err != nil {
return "", err
}
return items[idx], nil
}
Why this works: rand.Int(rand.Reader, max) generates cryptographically secure random integers in a range without modulo bias. It reads from crypto/rand and uses rejection sampling to ensure uniform distribution. For uint64 values, reading 8 bytes from crypto/rand and converting with binary.BigEndian.Uint64 provides full 64-bit entropy. These patterns are suitable for security-sensitive random selections, like choosing random records from a database for random sampling in security contexts. Unlike math/rand.Intn(), these use CSPRNG sources.