Spring Boot: 2024 JJWT 취약점(v0.12.5 이하) 및 'signWith(java.security.Key, io.jsonwebtoken.SignatureAlgorithm)' is deprecated 해결 (v0.12.0 이상)

Spring Boot: 2024 JJWT 취약점(v0.12.5 이하) 및 'signWith(java.security.Key, io.jsonwebtoken.SignatureAlgorithm)' is deprecated 해결 (v0.12.0 이상)

다 방법이 있습니다.

JJWT Impl의 취약점 발견

취약점 보고(CVE)

참고: signWith() 함수의 deprecated는 0.12.0 버전에 되어 CVE 보고와 무관합니다. 취약점은 사소한 것이며, 0.11 버전 이하를 사용하더라도 반드시 올려야 하는 것은 아닙니다.
벤더 측에서 릴리스한 버전을 권장하기 위하여 정보를 공유합니다.

해당 취약점은 CVE에 CVE-2024-31033로 보고되었습니다. (2024-03-27, 논쟁 있음)

이 보고의 내용은 이렇습니다. (논쟁)

0.12.5 이하 JJWT(Java JWT)는 특정 문자를 무시하므로 사용자에게 강력한 키(key)가 있다고 잘못 판단했을 수 있습니다.

JJWT 공급자는 다음처럼 이의를 제기합니다.

JJWT 사용 방식에서 사용자의 오류가 없는 한 '무시(ignores)' 동작이 어떤 버전에서도 발생할 수 없으며, 실제 테스트된 버전은 6년 이상 지나야 하기 때문에 이의를 제기하고 있습니다.

이 취약점의 영향을 받는 코드는 다음과 같습니다.

  • DefaultJwtParser 클래스 내의 setSigningKey() 메서드

  • DefaultJwtBuilder 클래스 내의 signWith() 메서드

새 버전 릴리스

MVN Repository에서 다음처럼 0.12.5 이하 버전은 모두 취약점 하나가 체크되어 있으며, 올해 6월 21일에 그 다음 버전인 0.12.6 버전이 릴리스되었습니다.


새 버전에서 유효한 방식 (v0.12.0 이상)

의존성 추가

다음처럼 v0.12.6으로 의존성을 추가합니다.

// build.gradle에서 관련 모듈의 dependencies
dependencies {
    // jjwt
    implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
}

JWT Provider 작성

deprecated 예시

// 0.11 버전에서 deprecated 되었습니다.
// (io.jsonwebtoken.SignatureAlgorithm, String)
.signWith(SignatureAlgorithm.HS256, secret)

// 0.12.0 버전에서 deprecated 되었습니다.
// (java.security.Key, io.jsonwebtoken.SignatureAlgorithm)
.signWith(secretKey, SignatureAlgorithm.HS256)

유효한 함수

// signWith(java.security.Key)
.signWith(secretKey)

전체 코드 예시 (generateToken() 함수 위주로 보세요.)

import example.demo.common.jwt.properties.DemoJwtProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtProvider {

    private final Key secretKey;
    private final long maxAge;

    // 구성 속성을 받아 올 때는 편한 방식(@Value 등)을 사용하면 됩니다.
    public JwtProvider(DemoJwtProperties demoJwtProperties) {
        secretKey = generateSecretKey(demoJwtProperties.secret());
        maxAge = demoJwtProperties.maxAge();
    }

    @Override
    public String generateToken(String subject, Map<String, ?> payload) {
        // header, payload, signature -> base64
        Claims claims = generateClaims(subject, payload);

        return Jwts.builder()
                .signWith(secretKey)
                .claims(claims)
                .compact();
    }

    // DemoJwtProperties 파일에 작성할 수도 있지만, 속성 파일에 외부 기술에 대한 종속성을 만들지 않기 위해 여기서 작성.
    private Key generateSecretKey(String secret) {
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        return Keys.hmacShaKeyFor(keyBytes);
    }

    private Claims generateClaims(String subject, Map<String, ?> payload) {
        Date now = new Date();
        Date expirationAt = new Date(now.getTime() + maxAge);

        return Jwts.claims()
                .subject(subject)
                .issuedAt(now)
                .expiration(expirationAt)
                .add(payload)
                .build();
    }
}