사용자 제어 JWT를 서명 검증 없이 파싱

사용자 제어 JWT를 서명 검증 없이 파싱

설명

Parser.ParseUnverified는 JWT의 헤더와 클레임을 디코딩하지만 서명을 검증하지 않습니다. golang-jwt/jwt/v5 문서도 이 메서드가 서명을 검증하지 않으며, 반환된 Token.Valid가 false라고 명시합니다.

사용자가 제어하는 토큰의 디코딩된 클레임을 서명 검증 없이 인증이나 인가에 사용하면 공격자가 sub, 역할, 권한 같은 값을 위조할 수 있습니다. 클레임을 신뢰하기 전에 별도의 서명 검증을 완료하는 제한된 파싱 흐름과 구분해야 합니다.

검증 없는 파싱의 사용 조건

ParseUnverified로 읽은 헤더와 클레임은 검증된 신원 정보가 아닙니다. 다음 두 파서 생성 방식도 모두 서명 검증 없이 데이터를 읽습니다.

go
parser := jwt.NewParser()
parser.ParseUnverified(rawToken, jwt.MapClaims{})

parser = &jwt.Parser{}
parser.ParseUnverified(rawToken, jwt.MapClaims{})

서명 검증이 이미 끝났거나, 데이터를 신뢰하기 전에 별도로 검증하는 흐름에서만 사용하세요. 아래 검증 예시는 github.com/golang-jwt/jwt/v5용이며 이전 주요 버전이나 github.com/dgrijalva/jwt-go는 지원 상태와 API를 따로 확인해야 합니다.

잠재적 영향

  • 위조된 식별자, 역할 또는 권한 클레임을 신뢰해 인증이나 인가를 우회할 수 있습니다.
  • 검증되지 않은 alg 또는 kid를 그대로 신뢰하면 공격자가 알고리즘이나 키 선택을 교란할 수 있습니다.
  • golang-jwt/jwt/v5 5.2.2 미만 버전은 적대적 토큰을 파싱할 때 과도한 메모리 할당을 유발할 수 있는 CVE-2025-30204의 영향을 받습니다.

해결 방법

  • ParseUnverified를 jwt.Parse 또는 jwt.ParseWithClaims로 바꾸고, 오류가 없으며 반환된 토큰이 nil이 아니고 Valid가 true일 때만 클레임을 신뢰하세요.
  • jwt.WithValidMethods로 허용할 서명 알고리즘을 제한하세요. 검증 키는 신뢰할 수 있는 설정이나 검증된 JWKS에서 가져오고, 토큰의 미검증 alg 또는 kid만으로 키나 알고리즘을 결정하지 마세요.
  • 애플리케이션이 의존하는 클레임을 명시적으로 검증하세요. v5에서는 jwt.WithExpirationRequired, jwt.WithIssuer, jwt.WithAudience 또는 jwt.WithAllAudiences를 사용할 수 있습니다. jwt.WithLeeway는 명시적인 시계 오차 정책이 있을 때만 사용하세요.
  • ParseUnverified는 서명 검증이 이미 완료되었거나, 어떤 클레임도 신뢰하기 전에 독립적으로 검증할 때만 사용하세요.
  • 5.3.1 이상 또는 그 이후의 유지 관리되는 v5 릴리스를 사용하세요. CVE-2025-30204만을 기준으로 한 최소 수정 버전은 5.2.2이지만, 장기적으로 특정 최소 버전에 고정하지 말고 보안 업데이트를 적용해야 합니다.

예시

변경 전

go
func unverifiedClaims(r *http.Request) (jwt.MapClaims, error) {
    rawToken := r.FormValue("token")
    token, _, err := jwt.NewParser().ParseUnverified(rawToken, jwt.MapClaims{})
    if err != nil {
        return nil, err
    }
    return token.Claims.(jwt.MapClaims), nil
}

rawToken은 사용자 요청에서 오지만 ParseUnverified는 서명을 검증하지 않습니다. 반환된 클레임을 신뢰하면 공격자가 값을 위조할 수 있습니다.

변경 후

go
func verifiedToken(rawToken string, verificationKey *rsa.PublicKey) (*jwt.Token, error) {
    token, err := jwt.Parse(
        rawToken,
        func(token *jwt.Token) (any, error) {
            // 실제 서비스에서는 발급자에 연결된 신뢰 가능한 설정이나
            // 검증된 JWKS 캐시에서 키를 선택합니다.
            return verificationKey, nil
        },
        jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()}),
        jwt.WithExpirationRequired(),
        jwt.WithIssuer("https://issuer.example"),
        jwt.WithAudience("api.example"),
    )
    if err != nil {
        return nil, fmt.Errorf("JWT verification failed: %w", err)
    }
    if token == nil || !token.Valid {
        return nil, errors.New("invalid JWT")
    }
    return token, nil
}

허용 알고리즘과 필수 클레임을 파서 옵션으로 고정하고, 신뢰할 수 있는 공개 키로 서명을 검증한 뒤 유효한 토큰만 반환합니다. 사용자 정의 클레임 구조체가 필요하면 같은 옵션과 키 정책을 jwt.ParseWithClaims에 적용합니다.

참조