암호학적으로 취약한 난수 생성기(PRNG) 사용

암호학적으로 취약한 난수 생성기(PRNG) 사용

설명

토큰, 세션 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로 표현합니다. 인코딩은 엔트로피를 늘리지 않으며, 토큰의 저장·만료·사용 검증은 별도 구현이 필요합니다.

참조