RSA 공개키는 이름 그대로 유출돼도 안전해야 하는 값이다. 그런데 특정 조건에서는 그 공개키 문자열 하나가 관리자 권한 토큰을 위조하는 재료로 그대로 쓰인다. JWT 검증 코드의 어느 지점에 그 조건이 숨어 있는지, RFC 7515·RFC 8725 스펙을 따라가며 짚어본다.
이전에 JWT는 왜 세 조각으로 나뉘고, 서버는 무엇을 검증하는가에서 "검증은 payload를 읽는 일이 아니라 믿어도 되는 조건을 확인하는 일"이라고 정리한 적이 있다. 이번 글은 그 조건 중 하나가 깨졌을 때 실제로 무슨 일이 벌어지는지에 대한 이야기다.
JWS(JSON Web Signature)는 Header.Payload.Signature 세 부분을 점으로 이어 붙인 문자열이고, 서명이 실제로 덮는 범위는 BASE64URL(Header) || '.' || BASE64URL(Payload) 전체다. 여기서 눈여겨봐야 할 점은 Header 자신도 서명 대상에 포함된다는 것이다. Header 안에는 어떤 알고리즘으로 서명했는지를 적는 alg 필드가 들어 있다.
문제는 이 alg 값을 "누가 정하느냐"다. 서명을 만드는 쪽은 당연히 자신이 쓴 알고리즘을 정확히 적어야 하지만, 검증하는 쪽이 이 값을 무조건 믿고 알고리즘을 고른다면, 알고리즘 선택권이 토큰을 보낸 쪽(즉 공격자가 통제할 수 있는 영역)으로 넘어가 버린다.
RFC 7515 §5.2가 정의하는 검증 8단계 중 마지막 단계는 "alg Header Parameter 값이 나타내는 방식으로 서명을 검증하라"고만 말한다. 그런데 §10.6과 §10.7은 정반대 방향에서 경고한다 — 검증자가 여러 알고리즘을 동시에 지원하면서 alg 헤더를 그대로 신뢰하면, 공격자가 다른 알고리즘으로 같은 서명값을 만족시키는 치환 공격이 성립한다는 것이다.
RFC 8725 §2.1은 실제로 보고된 사례(CVE-2015-9235로 알려져 있다)를 든다. alg를 RS256에서 HS256으로 바꾸면, 일부 라이브러리가 RSA 공개키 바이트열을 HMAC 공유 비밀로 그대로 재사용해 HMAC-SHA256 검증을 시도한다는 것이다. 흐름은 이렇다.
{"alg":"HS256"} 헤더와 원하는 payload({"sub":"admin"} 등)를 만든다.HMAC-SHA256(signingInput, key=공개키바이트)를 직접 계산해 서명으로 붙인다.verify(token, 공개키)를 호출하는데, 내부적으로 alg 헤더가 HS256이니 HMAC 검증기로 분기하고, 같은 공개키를 이번엔 "HMAC 비밀키"로 사용한다.키 자체는 바뀐 게 없다. 그 키를 "비대칭 검증용 공개 정보"로 쓰느냐 "대칭 MAC용 비밀 정보"로 쓰느냐를 결정하는 스위치가 오직 공격자가 채우는 alg 필드라는 게 이 공격의 핵심이다.
PyJWT 같은 라이브러리에서 이 취약점이 실제로 어떤 API 설계 차이로 막히는지 보면 이해가 빠르다.
import jwt
# 서버의 RSA 공개키 (원래 공개해도 안전하다고 가정된 값)
public_key = open("server_public.pem").read()
# 공격자: 공개키를 HMAC 비밀키로 취급해 위조 토큰을 만든다
forged = jwt.encode({"sub": "admin"}, public_key, algorithm="HS256")
# 취약한 패턴: 허용 알고리즘을 지정하지 않고 토큰의 alg를 그대로 믿는다
# (구버전 PyJWT는 이런 호출을 허용했다 — CVE-2015-9235)
jwt.decode(forged, public_key, algorithms=["HS256", "RS256"]) # 통과해버린다
# 안전한 패턴: 이 키는 RS256 검증에만 쓰겠다고 서버가 못 박는다
jwt.decode(forged, public_key, algorithms=["RS256"]) # InvalidSignatureError
두 번째와 세 번째 decode 호출의 차이가 RFC 8725 §3.1이 요구하는 것 그대로다. "라이브러리는 호출자가 지원할 알고리즘 집합을 지정할 수 있게 해야 하고, 그 외 알고리즘은 쓰면 안 되며, 키 하나는 정확히 하나의 알고리즘에만 묶여야 한다." 알고리즘을 고르는 주도권이 토큰(공격자 영역)이 아니라 서버 설정(서버 영역)에 있어야 안전하다.
alg 헤더는 서명 생성자의 선언일 뿐 검증자에게 강제력이 없는 값이라는 것, 그리고 안전한 검증은 "토큰이 뭐라고 하는지"가 아니라 "서버가 미리 정한 것과 일치하는지"만 확인해야 한다는 것이 이 공격이 남기는 결론이다. 다음으로 파고들 만한 지점은 JWKS의 kid 헤더를 파일 경로로 그대로 쓸 때 생기는 주입 공격, 그리고 JWE에서 alg/enc 조합을 조작하는 유사한 혼동 패턴이다.