설명
토큰, 세션 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비트) 난수 -> hex로 인코딩하면 32자리 문자열
b := make([]byte, 16)
// crypto/rand.Read는 암호학적으로 안전한 난수를 생성
if _, err := rand.Read(b); err != nil {
return "", err
}
// 사람이 사용하기 쉽도록 hex 문자열로 변환
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자리 hex로 표현합니다. 인코딩은 엔트로피를 늘리지 않으며, 토큰의 저장·만료·사용 검증은 별도 구현이 필요합니다.