Description
Using math/rand for tokens, session IDs or password-reset codes does not provide the randomness guarantees needed for unpredictable security values. Depending on the algorithm and seed, observed values or guessed seeds may reveal future output. Use crypto/rand for security-sensitive randomness.
Potential impact
- Predictable tokens or session IDs may enable account or session takeover.
- Guessed reset or verification codes may bypass authentication.
- Incorrect generation of cryptographic parameters that require unpredictability can weaken confidentiality and integrity.
Remediation
- Generate security-sensitive randomness with
crypto/rand; also satisfy the algorithm's length and uniqueness requirements for IVs and nonces. - Use
math/randonly where predictability does not affect security, such as statistical sampling. - Give reset tokens sufficient entropy, as in the example, and combine this with expiry, single-use handling and attempt limits.
- Use shared generation functions and review random length and encoding. Do not log real tokens.
Examples
The time-based seeding explanation applies to older behavior where Seed takes effect. Starting in Go 1.24, package-level math/rand.Seed does nothing by default. This change does not make math/rand a security API. fmt.Println is for demonstrating the result; do not print tokens in production logs.
Before
go
package main
import (
"fmt"
mrand "math/rand"
"time"
)
// Weak reset-token generation using math/rand
func generateResetTokenWeak() string {
// Legacy behavior: guessable time seed (a no-op by default since Go 1.24)
mrand.Seed(time.Now().UnixNano())
letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
b := make([]rune, 16) // 16-character token
for i := range b {
b[i] = letters[mrand.Intn(len(letters))]
}
return string(b)
}
func main() {
token := generateResetTokenWeak()
fmt.Println("reset token:", token)
}
After
go
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
)
// Reset-token generation using crypto/rand
func generateResetTokenSecure() (string, error) {
// 16 random bytes (128 bits) become 32 hexadecimal characters
b := make([]byte, 16)
// crypto/rand.Read generates cryptographically secure random bytes
if _, err := rand.Read(b); err != nil {
return "", err
}
// Represent the bytes as a hexadecimal string
return hex.EncodeToString(b), nil
}
func main() {
token, err := generateResetTokenSecure()
if err != nil {
panic(err)
}
fmt.Println("reset token:", token)
}
Explanation:
- Before: A nonsecurity random API generates a reset token. In environments that apply the time-based seed, a guessable seed adds risk.
- After:
crypto/randgenerates 16 random bytes, represented as 32 hexadecimal characters. Encoding does not increase entropy; storage, expiry and validation on use require separate implementation.