CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG) - Go
Overview
Weak PRNG vulnerabilities in Go occur when developers use the math/rand package for security-sensitive operations instead of the cryptographically secure crypto/rand package. Go provides two distinct random number generation APIs with very different security characteristics:
math/rand - Deterministic pseudorandom number generator based on a seed. Designed for simulations, games, and non-security purposes. Predictable if the seed is known. Uses algorithms like PCG or Linear Congruential Generators that are fast but not cryptographically secure.
crypto/rand - Cryptographically secure pseudo-random number generator (CSPRNG) that reads from the operating system's entropy pool (/dev/urandom on Unix, CryptGenRandom on Windows). Unpredictable even if previous outputs are known. Required for all security-sensitive operations.
The critical distinction is predictability: math/rand generates sequences that can be reproduced if the seed is known or guessed, while crypto/rand provides true randomness suitable for cryptographic operations. Using math/rand for security purposes (session tokens, CSRF tokens, encryption keys, password reset tokens) allows attackers to predict or brute-force values, leading to session hijacking, authentication bypass, and other attacks.
Primary Defence: Always use crypto/rand for security-sensitive operations. Reserve math/rand strictly for non-security use cases like shuffle algorithms, test data generation, or game mechanics.
Common Vulnerable Patterns
Session Token Generation with math/rand
// VULNERABLE - Predictable session tokens
package main
import (
"fmt"
"math/rand"
"net/http"
"time"
)
func init() {
// DANGEROUS: Seeding with time makes output predictable
rand.Seed(time.Now().UnixNano())
}
func generateSessionToken() string {
// VULNERABLE: math/rand produces predictable sequences
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
token := make([]byte, 32)
for i := range token {
token[i] = charset[rand.Intn(len(charset))]
}
return string(token)
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
// Generate session token
sessionToken := generateSessionToken()
// Set cookie
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: sessionToken,
Path: "/",
MaxAge: 3600,
HttpOnly: true,
})
fmt.Fprintf(w, "Session: %s", sessionToken)
}
// ATTACK: Attacker observes several tokens, determines seed,
// predicts future tokens to hijack sessions
Why this is vulnerable: math/rand is seeded with time.Now().UnixNano(), which has limited entropy (millisecond precision). An attacker who knows the approximate time of token generation can brute-force the seed (test timestamps within a reasonable range), reproduce the random sequence, and predict session tokens. With enough observed tokens, cryptanalysis techniques can recover the internal state of the generator, making all future tokens predictable.
CSRF Token with Predictable randomness
// VULNERABLE - Weak CSRF token generation
import (
"fmt"
"math/rand"
)
func generateCSRFToken() string {
// VULNERABLE: rand.Intn() is predictable
token := fmt.Sprintf("%016x", rand.Int63())
return token
}
func formHandler(w http.ResponseWriter, r *http.Request) {
csrfToken := generateCSRFToken()
// Store in session and render form
w.Write([]byte(fmt.Sprintf(`
<form method="POST">
<input type="hidden" name="csrf_token" value="%s">
<button>Submit</button>
</form>
`, csrfToken)))
}
// ATTACK: Attacker predicts CSRF tokens, bypasses protection
Why this is vulnerable: rand.Int63() returns values from a predictable sequence. An attacker can collect several CSRF tokens from legitimate requests, analyze the pattern, and predict future tokens. This allows forging CSRF tokens for other users, bypassing the CSRF protection entirely. The 16 hex character format (64 bits) seems secure, but the deterministic nature of math/rand reduces effective entropy to the seed size.
Encryption Key Derivation
// VULNERABLE - Using math/rand for key generation
import (
"crypto/aes"
"crypto/cipher"
"math/rand"
)
func generateEncryptionKey() []byte {
// DANGEROUS: Encryption keys must be cryptographically random
key := make([]byte, 32) // 256-bit key
for i := range key {
key[i] = byte(rand.Intn(256))
}
return key
}
func encryptData(plaintext []byte) ([]byte, error) {
key := generateEncryptionKey()
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// Create GCM cipher
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// Generate nonce with math/rand - ALSO VULNERABLE!
nonce := make([]byte, gcm.NonceSize())
for i := range nonce {
nonce[i] = byte(rand.Intn(256))
}
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
return ciphertext, nil
}
// ATTACK: Attacker can predict key or nonce, decrypt data
Why this is vulnerable: Encryption security relies on key unpredictability. Using math/rand for key generation creates keys that can be brute-forced by testing likely seeds. Additionally, using math/rand for the AES-GCM nonce is catastrophic - reusing nonces with the same key in GCM completely breaks encryption and allows authentication bypass. An attacker who can predict the nonce can recover the authentication key.
Password Reset Token
// VULNERABLE - Predictable password reset tokens
import (
"fmt"
"math/rand"
"time"
)
func generateResetToken(email string) string {
// VULNERABLE: Attacker can predict tokens for other users
rand.Seed(time.Now().UnixNano())
token := fmt.Sprintf("%s_%d", email, rand.Int63())
return token
}
func requestPasswordReset(email string) {
resetToken := generateResetToken(email)
// Store in database
// Send email with reset link
fmt.Printf("Reset link: /reset?token=%s\n", resetToken)
}
// ATTACK: Attacker requests password reset for their account,
// observes token, predicts tokens for victim accounts
Why this is vulnerable: Password reset tokens must be unguessable to prevent account takeover. Using math/rand allows an attacker to request resets for their own account, determine the seed from the generated tokens, then predict tokens for victim accounts. The attacker can then use predicted tokens to reset passwords on other accounts without access to those email addresses.
Secure Patterns
Secure Session Token Generation
// SECURE - Cryptographically strong session tokens
package main
import (
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"net/http"
)
func generateSecureToken(length int) (string, error) {
// SECURE: crypto/rand provides cryptographic randomness
bytes := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
return "", fmt.Errorf("failed to generate token: %w", err)
}
// Encode as URL-safe base64
token := base64.URLEncoding.EncodeToString(bytes)
return token, nil
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
// Generate 32-byte (256-bit) session token
sessionToken, err := generateSecureToken(32)
if err != nil {
http.Error(w, "Internal server error", 500)
return
}
// Set secure cookie
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: sessionToken,
Path: "/",
MaxAge: 3600,
HttpOnly: true,
Secure: true, // HTTPS only
SameSite: http.SameSiteStrictMode,
})
// Store session in database/cache
// ...
w.WriteHeader(http.StatusOK)
}
Why this works: crypto/rand.Reader reads from the operating system's CSPRNG (/dev/urandom), which pools entropy from hardware events, making output unpredictable. io.ReadFull() ensures all bytes are filled - it returns an error if insufficient entropy is available. The 32-byte token provides 256 bits of entropy, making brute-force attacks computationally infeasible. Base64URL encoding creates safe tokens for URLs and cookies. Error handling ensures tokens are never generated when randomness is compromised.
CSRF Token Generation
// SECURE - Cryptographically secure CSRF tokens
import (
"crypto/rand"
"encoding/hex"
"fmt"
)
func generateCSRFToken() (string, error) {
// SECURE: 32 bytes = 256 bits of entropy
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("crypto/rand failed: %w", err)
}
// Hex encoding for HTML form compatibility
token := hex.EncodeToString(bytes)
return token, nil
}
func formHandler(w http.ResponseWriter, r *http.Request) {
csrfToken, err := generateCSRFToken()
if err != nil {
http.Error(w, "Internal error", 500)
return
}
// Store in session (e.g., in session store or encrypted cookie)
// ... sessionStore.Set(session.ID, csrfToken)
// Render form with token
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, `
<form method="POST" action="/submit">
<input type="hidden" name="csrf_token" value="%s">
<input type="text" name="data">
<button type="submit">Submit</button>
</form>
`, csrfToken)
}
func submitHandler(w http.ResponseWriter, r *http.Request) {
// Verify CSRF token
submittedToken := r.FormValue("csrf_token")
// storedToken := sessionStore.Get(session.ID)
// Constant-time comparison to prevent timing attacks
// if subtle.ConstantTimeCompare([]byte(submittedToken), []byte(storedToken)) != 1 {
// http.Error(w, "Invalid CSRF token", 403)
// return
// }
// Process form...
}
Why this works: crypto/rand.Read() fills the byte slice with cryptographically secure random data. Each CSRF token has 256 bits of entropy, making it impossible to guess or predict even with knowledge of other tokens. Hex encoding produces tokens safe for HTML forms. The token is verified server-side using constant-time comparison to prevent timing attacks that could leak information about the correct token.
Secure Encryption Key and Nonce
// SECURE - Cryptographic key and nonce generation
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"io"
)
func generateKey() ([]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 encryptData(plaintext []byte, key []byte) ([]byte, error) {
if len(key) != 32 {
return nil, errors.New("key must be 32 bytes")
}
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 and authenticate
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
return ciphertext, nil
}
func decryptData(ciphertext []byte, key []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
}
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return nil, errors.New("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
Why this works: Both the AES-256 key and the GCM nonce are generated using crypto/rand, ensuring unpredictability. The 32-byte key provides maximum AES security. The nonce is prepended to the ciphertext, allowing decryption without storing it separately. Each encryption uses a unique random nonce, preventing nonce reuse which would catastrophically break GCM security. io.ReadFull ensures the entire byte slice is filled with random data.
Secure API Key Generation
// SECURE - API key generation for authentication
import (
"crypto/rand"
"encoding/base32"
"strings"
)
func generateAPIKey() (string, error) {
// SECURE: 20 bytes = 160 bits (similar to UUID v4)
bytes := make([]byte, 20)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
// Base32 encoding (uppercase, no padding) for readability
key := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(bytes)
// Format: XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXX
// Makes keys easier to read and verify
key = formatAPIKey(key)
return key, nil
}
func formatAPIKey(key string) string {
// Insert dashes every 5 characters for readability
var formatted strings.Builder
for i, char := range key {
if i > 0 && i%5 == 0 {
formatted.WriteRune('-')
}
formatted.WriteRune(char)
}
return formatted.String()
}
// Example usage:
// apiKey, err := generateAPIKey()
// // Result: "7N4QK-J2MBV-QX8RP-WLZTG-N6V3M-92KHL-XQYZ"
Why this works: 160-bit API keys provide sufficient entropy to prevent brute-force attacks while being shorter than 256-bit session tokens (API keys are often manually entered or stored). Base32 encoding avoids ambiguous characters (0/O, 1/l/I) making keys more user-friendly for manual entry. The formatted output with dashes improves readability. crypto/rand ensures each API key is unique and unpredictable.
Password Reset Token with Timeout
// SECURE - Time-limited password reset tokens
import (
"crypto/rand"
"encoding/base64"
"fmt"
"time"
)
type ResetToken struct {
Token string
Email string
ExpiresAt time.Time
}
func generateResetToken(email string) (*ResetToken, error) {
// SECURE: 32-byte cryptographically random token
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return nil, err
}
token := base64.URLEncoding.EncodeToString(bytes)
return &ResetToken{
Token: token,
Email: email,
ExpiresAt: time.Now().Add(1 * time.Hour), // 1-hour expiry
}, nil
}
func requestPasswordReset(email string) error {
resetToken, err := generateResetToken(email)
if err != nil {
return fmt.Errorf("token generation failed: %w", err)
}
// Store in database with expiry
// db.SaveResetToken(resetToken)
// Send email
// sendEmail(email, fmt.Sprintf("/reset?token=%s", resetToken.Token))
return nil
}
func validateResetToken(token string, email string) (bool, error) {
// Retrieve from database
// storedToken := db.GetResetToken(token)
// Check expiration
// if time.Now().After(storedToken.ExpiresAt) {
// return false, errors.New("token expired")
// }
// Verify email match
// if storedToken.Email != email {
// return false, errors.New("email mismatch")
// }
// Delete token after use (one-time use)
// db.DeleteResetToken(token)
return true, nil
}
Why this works: The reset token is generated with crypto/rand, providing 256 bits of unpredictable entropy. Tokens are time-limited (1 hour), reducing the window of opportunity for attacks. Tokens are single-use - deleted after consumption - preventing replay attacks. Email verification ensures tokens can only be used for the intended account. This combination makes it computationally infeasible to guess or predict valid tokens.
Framework-Specific Guidance
Gin with Secure Session Management
// SECURE - Gin with secure session tokens
package main
import (
"crypto/rand"
"encoding/base64"
"io"
"net/http"
"github.com/gin-gonic/gin"
)
func generateSessionID() (string, error) {
b := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
func main() {
r := gin.Default()
r.POST("/login", loginHandler)
r.GET("/dashboard", authMiddleware(), dashboardHandler)
r.Run(":8080")
}
func loginHandler(c *gin.Context) {
// Authenticate user (check username/password)
username := c.PostForm("username")
password := c.PostForm("password")
// ... authenticate(username, password) ...
sessionID, err := generateSessionID()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Login failed"})
return
}
// Store session in Redis/database
// sessionStore.Set(sessionID, username, 3600)
c.SetCookie(
"session_id",
sessionID,
3600,
"/",
"",
true, // Secure (HTTPS only)
true, // HttpOnly
)
c.JSON(http.StatusOK, gin.H{"status": "logged in"})
}
func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
sessionID, err := c.Cookie("session_id")
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
// Validate session
// username := sessionStore.Get(sessionID)
// if username == "" {
// c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid session"})
// return
// }
// c.Set("username", username)
c.Next()
}
}
func dashboardHandler(c *gin.Context) {
// username := c.GetString("username")
c.JSON(http.StatusOK, gin.H{"message": "Welcome to dashboard"})
}
Why this works: Session IDs are generated with crypto/rand, making session hijacking through prediction impossible. Gin's cookie functions set secure flags (HttpOnly, Secure) preventing JavaScript access and ensuring HTTPS-only transmission. Sessions are stored server-side, allowing revocation. The authentication middleware validates sessions on every request.
Echo with CSRF Middleware
// SECURE - Echo with built-in CSRF protection
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
// SECURE: Echo's CSRF middleware uses crypto/rand
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
TokenLength: 32, // 32 bytes = 256 bits
TokenLookup: "form:csrf_token", // Look in form field
CookieName: "_csrf",
CookieSecure: true,
CookieHTTPOnly: true,
CookieSameSite: http.SameSiteStrictMode,
}))
e.GET("/form", showFormHandler)
e.POST("/submit", submitHandler)
e.Start(":8080")
}
func showFormHandler(c echo.Context) error {
// Get CSRF token from context (auto-generated by middleware)
csrfToken := c.Get("csrf").(string)
html := `
<form method="POST" action="/submit">
<input type="hidden" name="csrf_token" value="` + csrfToken + `">
<input type="text" name="data">
<button type="submit">Submit</button>
</form>
`
return c.HTML(http.StatusOK, html)
}
func submitHandler(c echo.Context) error {
// CSRF middleware automatically validates token
data := c.FormValue("data")
return c.JSON(http.StatusOK, map[string]string{
"status": "received",
"data": data,
})
}
Why this works: Echo's CSRF middleware generates tokens using crypto/rand internally, providing cryptographically secure protection. The middleware automatically validates tokens on state-changing requests (POST, PUT, DELETE). Tokens are stored in secure, HTTP-only cookies, preventing JavaScript access. The 32-byte (TokenLength: 32) parameter ensures 256 bits of entropy.