infiltr8 Lab
Draft · 검수 전 · 심층판
Home/ CVE/ 웹·서버/ fast-jwt 공백 우회
CVE · 웹·서버 · 인증우회 · CVE-2026-34950
심층 분석 연구 분석

공개키를 알아보지 못한 순간, 비밀키가 되었다

fast-jwt(CVE-2026-34950) — 약 2년 4개월 전 패치된 JWT 알고리즘 혼동 공격이, 공개키 앞의 공백 한 글자로 그대로 되살아났습니다. 이 글은 공개된 권고문과 오픈소스 저장소의 실제 커밋을 직접 대조해, 왜 첫 패치가 불완전했는지를 우리 관점에서 분석합니다.

이 글은 GitHub Security Advisory GHSA-mvf2-f6gm-w987(CVE-2026-34950)과 fast-jwt 오픈소스 저장소(MIT)의 실제 커밋(v6.1.0 → v6.2.0, 태그 커밋 986a77a → e07b1fd)을 저희가 직접 diff로 대조해 재구성한 연구 글입니다. §7·§9의 코드 발췌는 저장소 원문 그대로이며 라인번호까지 직접 확인했습니다. 공격 시나리오(§8)는 권고문에 실린 PoC의 구조를 개념적으로 재서술한 것으로, 완성형 공격 스크립트나 실행 가능한 페이로드는 싣지 않았습니다. 2026년 4월 공개된 최신 CVE라 커뮤니티 2차 분석이 아직 많지 않습니다 — 실제 악용 사례나 다른 JWT 라이브러리로의 동일 변형 여부는 공개 정보로 확인되지 않아 미확인으로 남깁니다. 초고 작성 이후 공개된 후속 권고(GHSA-ww5h-9m49-7xx4, 2026-07-28)를 반영해, 6.2.0~6.2.4도 여전히 재취약하며 실제 안전 버전은 6.3.0이라는 점을 본문·remediation에 정정했습니다.

infiltr8 team 직접 분석 #JWT#algorithm-confusion#패치완전성
무엇Node.js JWT 라이브러리 fast-jwt(≤6.1.0)의 무인증 인증 우회입니다. RS256으로 서명하도록 설계된 서버가, 공개키 문자열 앞에 공백 한 글자만 있어도 HS256으로 위조한 토큰을 그대로 받아들입니다. CVSS 9.1.
원리2023년 패치가 막은 것은 "문자열 중간에 PEM 헤더가 섞여 들어오는 경우"뿐이었습니다. "헤더 앞에 공백이 오는 경우"는 그대로 열려 있었습니다.
핵심개인키를 판별하는 함수는 이미 trim()을 쓰고 있었는데, 그 습관이 쌍둥이 함수인 공개키 판별 함수로는 옮겨가지 않았습니다 — 패치의 비대칭이 취약점의 전부입니다.

01개요

fast-jwt(Node.js npm 패키지, nearform 개발)의 JWT 서명 검증 로직에서 발생하는 무인증 인증 우회입니다. RS256(비대칭)으로 서명하도록 설계된 서버가, 검증에 쓰는 공개키 문자열 앞에 공백 한 글자만 있어도 HS256(대칭)으로 위조된 토큰을 그대로 통과시킵니다.

더 특이한 점은 이것이 완전히 새로운 결함이 아니라는 것입니다. 2023년 같은 클래스의 취약점(CVE-2023-48223)이 한 번 패치됐는데, 그 패치가 정확히 이 경로(공백 문자)를 막지 못해 약 2년 4개월 뒤 같은 결과가 다시 재현됐습니다. 그래서 이번 권고문의 제목도 "incomplete fix"입니다.

CVSS: 9.1

관련 식별자: CVE-2023-48223(원 취약점, GHSA-c2ff-88x2-x9pg · 공개 2023-11-20) / GHSA-mvf2-f6gm-w987(이번 재개봉, CVE-2026-34950 · 공개 2026-04-02, NVD 등재 2026-04-06) / GHSA-ww5h-9m49-7xx4(6.2.x 재취약, 공개 2026-07-28).

02영향받는 버전

대상취약 버전안전 버전
fast-jwt (npm, 이번 취약점)≤ 6.1.0 (및 후속 미완결 패치인 6.2.0~6.2.4, GHSA-ww5h-9m49-7xx4)6.3.0 이상
fast-jwt (2023년, 원 취약점 CVE-2023-48223)< 3.3.23.3.2 이상 — 그러나 그 패치의 빈틈이 6.1.0까지 그대로 남아있었습니다

03배경 — RS256과 HS256은 "키의 성격"이 다르다

JWT는 헤더에 alg 필드를 둬 어떤 알고리즘으로 서명했는지를 스스로 적어 둡니다. HS256(HMAC)은 서명자와 검증자가 같은 비밀 값을 공유하는 대칭 방식이고, RS256(RSA)은 개인키로 서명하고 공개키로 검증하는 비대칭 방식입니다.

문제는 검증기가 이 alg 필드를 어디까지 믿느냐입니다. RS256용으로 배포된 공개키를 검증기가 실수로 "HMAC 비밀"로도 받아준다면, 그 공개키를 아는 사람 누구나 같은 값으로 HMAC 서명을 계산해 HS256 토큰을 위조할 수 있습니다. 공개키는 이름 그대로 공개된 값이라, 이 위조에는 아무 비밀도 필요하지 않습니다.

이것이 2015년 처음 널리 알려진 알고리즘 혼동(algorithm confusion) 공격입니다. JWT 라이브러리라면 한 번쯤 거쳐 가는 고전적 취약점 클래스이고, fast-jwt도 2023년(CVE-2023-48223)에 이미 한 번 패치한 이력이 있습니다.

그래서 검증기 내부에는 반드시 "이 키 문자열이 대칭키인지 비대칭키인지"를 판별하는 코드가 있어야 합니다. fast-jwt는 이 판별을, 키 문자열이 PEM 헤더(-----BEGIN PUBLIC KEY-----)로 시작하는지를 정규식으로 검사해서 합니다 — 그리고 이 판별 로직 자체가 이번 취약점의 무대입니다.

04취약점 발생 원인

취약점의 근본 성격은 "검증 실패가 아니라 검증 오분류"입니다. 아래 세 미흡점이 차례로 얽혀, 공개키 판별 실수 하나가 인증 우회로까지 이어집니다.

1"^로 시작해야 공개키다" — 그런데 앞에 공백이 있으면?

핵심

공개키 판별 정규식이 문자열 맨 앞(^)에서 시작하는 PEM 헤더만 인정합니다. 앞에 공백이 오면 매칭이 실패하고, 실패한 결과는 조용히 "평범한 비밀 문자열"로 처리됩니다.

fast-jwt는 검증 시점에 performDetectPublicKeyAlgorithms(key)를 호출해 이 키로 검증할 수 있는 알고리즘 목록을 정합니다. 이 함수는 key.match(publicKeyPemMatcher)로 문자열이 -----BEGIN PUBLIC KEY-----(또는 RSA PUBLIC KEY)로 시작하는지를 검사합니다.

function performDetectPublicKeyAlgorithms(key) { const publicKeyPemMatch = key.match(publicKeyPemMatcher) if (key.match(privateKeyPemMatcher)) { throw new TokenError(TokenError.codes.invalidKey, 'Private keys are not supported for verifying.') } else if (publicKeyPemMatch && publicKeyPemMatch[1] === 'RSA') { // pkcs1 format - Can only be RSA key return rsaAlgorithms } else if (!publicKeyPemMatch && !key.includes(publicKeyX509CertMatcher)) { // Not a PEM, assume a plain secret return hsAlgorithms }

fast-jwt 실제 소스(MIT) — src/crypto.js · performDetectPublicKeyAlgorithms() (v6.1.0, 커밋 986a77a). 127행이 trim 없이 매칭하는 곳, 134~136행이 오분류의 귀결입니다.

그런데 keytrim()하지 않고 그대로 정규식에 넣습니다. PostgreSQL/MySQL 텍스트 컬럼, YAML 블록 스칼라, 개행이 섞인 환경변수처럼 키 문자열 앞에 개행이나 공백이 붙는 경우는 실무에서 드물지 않습니다.

앞에 공백이 하나만 있어도 ^ 앵커는 실패합니다. publicKeyPemMatchnull이 되고, 코드는 "PEM 형식이 아니니 그냥 비밀 문자열이겠지"라는 분기로 빠져 hsAlgorithms(HMAC 계열)를 돌려줍니다. RSA 공개키가 HMAC 비밀로 오분류되는 순간입니다.

왜 이렇게 됐나정규식 앵커(^)는 "문자열이 정확히 여기서 시작한다"는 강한 전제를 요구합니다. 이 전제가 실제 입력(개행이 섞인 키)과 어긋날 수 있다는 걸 이 함수는 검토하지 않았습니다.

2쌍둥이 함수인데, 한쪽에만 trim()이 있었다

핵심

서명 경로(개인키 판별)는 이미 trim()을 쓰고 있었습니다. 검증 경로(공개키 판별)만 그 습관이 옮겨가지 않았습니다 — 이 비대칭이 "불완전한 패치"의 정체입니다.

fast-jwt에는 구조가 거의 같은 두 함수가 나란히 있습니다. performDetectPrivateKeyAlgorithm()(서명용, 개인키 판별)과 performDetectPublicKeyAlgorithms()(검증용, 공개키 판별)입니다.

function performDetectPrivateKeyAlgorithm(key) { if (key.match(publicKeyPemMatcher) || key.includes(publicKeyX509CertMatcher)) { throw new TokenError(TokenError.codes.invalidKey, 'Public keys are not supported for signing.') } const pemData = key.trim().match(privateKeyPemMatcher)

fast-jwt 실제 소스(MIT) — src/crypto.js · performDetectPrivateKeyAlgorithm() (v6.1.0, 커밋 986a77a). 79행에 있는 trim()이 옆의 공개키 판별 함수엔 없습니다.

아래 74~80행을 보면, 개인키 판별 함수는 key.trim().match(privateKeyPemMatcher)로 이미 앞뒤 공백을 제거한 뒤 매칭합니다. 반면 앞서 본 공개키 판별 함수(W1)는 key를 trim하지 않은 채로 씁니다.

2023년 패치(CVE-2023-48223)는 공개키 판별을 단순 .includes() 문자열 포함검사에서 ^로 앵커된 정규식 .match()로 바꿨습니다. "문자열 중간에 PEM 헤더가 우연히 섞여 들어와도 공개키로 오인식되던 문제"는 이걸로 막혔습니다. 그런데 앵커드 매칭이 성립하려면 입력이 실제로 그 위치에서 시작해야 한다는 새 전제가 생겼고, 그 전제를 지키는 코드(trim)는 개인키 쪽에만 있었습니다.

왜 이렇게 됐나패치 작성자는 신고된 PoC(문자열 중간에 헤더가 섞이는 경우)를 정확히 막았습니다. 다만 "같은 클래스의 판별 함수가 형제로 하나 더 있고, 거기엔 이미 다른 정규화 습관이 있다"는 대칭성 점검까지는 하지 않았습니다.

3"algorithms를 안 정하면, 키가 알고리즘을 정한다"

핵심

호출자가 algorithms 옵션을 명시하지 않으면, 검증기는 방금 오분류된 판별 결과를 그대로 "허용 알고리즘 목록"으로 씁니다. 분류가 틀리면 정책도 함께 틀립니다.

createVerifier({ key })algorithms 옵션 없이 호출하는 것은 fast-jwt 문서에도 나오는 흔한 사용법입니다 — 키만 주면 알고리즘은 라이브러리가 알아서 판별해 준다는 편의 기능입니다.

if (key && keyType !== 'function') { // Detect the private key - If the algorithms were known, just verify they match, otherwise assign them const availableAlgorithms = detectPublicKeyAlgorithms(key) if (allowedAlgorithms.length) { checkAreCompatibleAlgorithms(allowedAlgorithms, availableAlgorithms) } else { allowedAlgorithms = availableAlgorithms } key = prepareKeyOrSecret(key, availableAlgorithms[0] === hsAlgorithms[0]) }

fast-jwt 실제 소스(MIT) — src/verifier.js · createVerifier() (v6.1.0, 커밋 986a77a). algorithms 미지정 시 키 판별 결과가 그대로 허용 목록이 됩니다.

내부적으로는 W1의 detectPublicKeyAlgorithms(key) 반환값을 그대로 allowedAlgorithms에 대입합니다(410행 부근). 공개키가 오분류돼 hsAlgorithms가 반환됐다면, 이제부터 이 검증기는 HS256 토큰을 정책적으로 허용합니다.

이어서 prepareKeyOrSecret(key, availableAlgorithms[0] === hsAlgorithms[0])가 실행되며, 오분류된 키는 createSecretKey(key)로 — 즉 공개키 PEM 텍스트 그 자체가 HMAC 비밀키 객체로 — 변환됩니다.

왜 이렇게 됐나이 설계 자체는 편의 기능으로 합리적입니다. 다만 그 편의가 성립하려면 키 판별이 항상 정확하다는 전제가 필요한데, W1의 판별 함수는 그 전제를 지키지 못했습니다. 판별 오류가 여기서 곧바로 "검증 정책"으로 승격됩니다.

분석 결론

세 미흡점처럼 보이지만, 무게 중심은 W1(공개키 판별의 trim 부재) 하나입니다. W2는 W1이 "왜 2년여 전에 이미 고칠 기회가 있었는데 놓쳤는가"를 설명하는 구조적 원인이고, W3은 W1의 오분류가 "왜 에러가 아니라 조용한 인증 우회로 이어지는가"를 설명하는 증폭 경로입니다.

그래서 이번 재패치(PR #598)는 W1 함수에 새로 trim()을 추가하는 데서 끝나지 않았습니다 — W2(개인키 판별 함수)에도 미처 다 옮겨지지 않은 트림 경로(ASN.1 디코드 호출부)가 남아 있었고, 같은 PR이 그 지점까지 함께 정리했습니다. 즉 W2도 "이미 완벽했던 형제"가 아니라 부분적으로만 고쳐져 있던 상태였습니다.

한 문장으로 묶으면 이렇습니다. 2023년 패치는 신고된 재현 사례 하나를 막았을 뿐, 그 재현 사례가 성립하지 않기 위한 일반 원칙(입력을 정규화한 뒤 판별한다)을 코드베이스 전체에 강제하지는 못했습니다. 그 틈이 그대로 2년여를 기다렸습니다.

05공격 과정

5.1 공격 과정 요약

서버가 개행 포함 공개키를 보관 DB 텍스트 컬럼·YAML 블록 스칼라 등에서 흔함 ① 정규화 없이 키 문자열을 그대로 넘김 createVerifier({ key }) 생성 · algorithms 미지정 detectPublicKeyAlgorithms(key) 호출 ② 공개키 판별 함수가 앞 공백을 trim하지 않음(W1) 핵심 지점 ^ 앵커 매칭 실패 → RSA 공개키를 HMAC 비밀로 오분류 performDetectPublicKeyAlgorithms → hsAlgorithms ③ algorithms 미지정이라 오분류 결과를 그대로 신뢰(W3) 오분류 결과가 그대로 허용 알고리즘 목록이 됨 공개키 PEM 텍스트 → createSecretKey() ④ 공개키는 원래 공개된 값 — 공격자도 알고 있음 공격자가 (공개된) 공개키로 HS256 서명 위조 개념 설명 — 실제 페이로드 아님 ⑤ HS256이 허용목록에 있어 서명 검증 통과 위조 토큰이 검증 통과 — 인증 우회 Authentication Bypass · 무인증
공격 흐름도 (직접 작성)

5.2 상세 공격 과정

서버 쪽 흐름부터 봅니다. 애플리케이션은 RSA 키 쌍을 생성해 개인키로는 createSigner({ algorithm: 'RS256', key: privateKey })로 토큰을 서명하고, 공개키로는 createVerifier({ key: publicKey })를 만들어 토큰을 검증합니다. 이 자체는 표준적인 RS256 사용법입니다.

문제는 이 publicKey 문자열이 어디서 왔느냐입니다. 데이터베이스 텍스트 컬럼, YAML 설정, 환경변수 등에서 읽어 오면 앞에 개행 하나가 붙는 일이 실무에서 드물지 않습니다. createVerifier는 초기화 시점에 이 문자열을 그대로 detectPublicKeyAlgorithms(key)에 넘깁니다.

W1에서 본 것처럼 publicKeyPemMatcher^ 앵커가 개행 때문에 실패하고, 함수는 hsAlgorithms를 돌려줍니다. algorithms 옵션을 명시하지 않았다면(W3) 이 값이 그대로 allowedAlgorithms가 되고, 키 객체는 createSecretKey()로 만들어진 HMAC 비밀키가 됩니다.

이 시점부터 서버는 "이 공개키 바이트열을 아는 사람이면 누구든 HS256 토큰에 유효하게 서명할 수 있는" 상태가 됩니다. 그런데 공개키는 정의상 비밀이 아닙니다 — 공개 API 응답, JWKS 엔드포인트, 이전에 발급받은 토큰의 헤더 등 여러 경로로 노출돼 있을 수 있습니다.

공격자는 이 공개키 값을 HMAC-SHA256 비밀로 사용해, alg: "HS256"과 원하는 페이로드로 서명을 계산합니다(구체적 계산 절차는 여기서 서술하지 않습니다 — 개념 수준입니다). 이 위조 토큰을 서버에 보내면, 검증기는 "HS256은 허용 알고리즘이고, HMAC 서명도 일치한다"고 판단해 그대로 통과시킵니다.

5.3 미흡점 ↔ 공격 흐름 매핑

미흡점(§4)발화 지점(§5.2)여는 프리미티브→ 다음으로 잇는 것
W1 — 공백 미처리(trim 부재)공개키 판별 함수 호출 시점(B→C)RSA 공개키 → HMAC 비밀로 오분류허용 알고리즘 목록에 HS256이 편입
W2 — 쌍둥이 함수 비대칭(2023년 패치의 빈틈)2023년 패치 당시 공개키 판별 함수만 trim 누락같은 취약점 클래스가 구조적으로 재개봉 가능한 상태W1이 약 2년 4개월 뒤에도 그대로 재현
W3 — algorithms 미지정 시 판별 결과를 그대로 신뢰(C→D)createVerifier 초기화(또는 콜백 키 해석) 시점판별 오류가 검증 정책 자체로 승격공개키가 HMAC 비밀키 객체로 생성
(귀결)HMAC-SHA256 서명 비교(D→E→F)공격자 위조 서명이 검증 통과인증 우회 확정 — 무인증·무자격증명

06취약점 패치

src/crypto.js·performDetectPublicKeyAlgorithms() · performDetectPrivateKeyAlgorithm()·PR #598(9f87f4f · 50c0054 · abd74f6) · v6.2.0
function performDetectPublicKeyAlgorithms(key) { const publicKeyPemMatch = key.match(publicKeyPemMatcher) const trimmedKey = key.trim() const publicKeyPemMatch = trimmedKey.match(publicKeyPemMatcher) if (key.match(privateKeyPemMatcher)) { if (trimmedKey.match(privateKeyPemMatcher)) { } else if (!publicKeyPemMatch && !key.includes(publicKeyX509CertMatcher)) { } else if (!publicKeyPemMatch && !trimmedKey.includes(publicKeyX509CertMatcher)) { }

공개키 판별에서 정규식 앵커(^)가 문자열 맨 앞만 검사하는데, 앞에 공백이 있으면 실패해 RSA 공개키가 HMAC 비밀로 오분류되기 때문입니다. W1이 성립하는 정확한 그 지점입니다.

어떻게PR #598은 두 함수를 함께 고쳤습니다. performDetectPublicKeyAlgorithms()(W1)에는 애초에 trim이 전혀 없었으므로, 함수 시작에서 trimmedKey = key.trim()을 새로 계산해 이후 모든 정규식 매칭·includes() 검사·X.509 변환에 일관되게 적용했습니다. performDetectPrivateKeyAlgorithm()(W2)은 PEM 판별 단계(key.trim().match(...))에는 이미 trim이 있었지만, 그 뒤 실제로 키를 디코드하는 ECPrivateKey.decode(key) / PrivateKey.decode(key) 호출에는 트림되지 않은 원본 key가 여전히 쓰이고 있었습니다. PR #598은 이 지점도 decode(trimmedKey)로 함께 고쳤습니다(커밋 메시지에 "gaps in EC/PKCS8 decoding") — W2도 완벽한 형제가 아니라 같은 PR에서 함께 보강된 대상입니다.

비고약 2년 4개월 전 패치(커밋 15a6e92, v3.3.2, CVE-2023-48223)는 단순 문자열 포함검사(.includes())를 앵커드 정규식(.match(/^.../))으로 바꿔 "문자열 중간에 PEM 헤더가 섞여도 공개키로 오인식되는" 문제를 막았습니다. 좋은 개선이었지만, 앵커드 매칭이 요구하는 새 전제(입력이 정확히 그 위치에서 시작해야 함)를 공개키 판별 함수 자신은 지키지 않았습니다 — 개인키 판별 함수는 부분적으로만 그 전제를 맞추고 있었는데도요. 이번 재패치(v6.2.0)는 그 비대칭을 없앤 것이지, 새로운 방어 개념을 도입한 것이 아닙니다. 다만 이 v6.2.0 재패치조차 끝이 아니었습니다 — trim()이 ECMAScript 공백 문자만 걸러내는 한계 때문에 2026년 7월 또 다른 우회(GHSA-ww5h-9m49-7xx4, 6.2.0~6.2.4 재취약)가 확인됐고, 최종 해결은 v6.3.0에서야 이뤄졌습니다.

07대응방안

fast-jwt 6.3.0 이상으로 올리는 것이 근본 해결입니다. 주의: 이번 CVE의 첫 재패치인 6.2.0~6.2.4도 완전하지 않았습니다 — trim()이 ECMAScript 공백 문자만 제거해서, 제어문자(U+0000~U+001F, U+007F)나 # 같은 비공백 바이트를 키 앞에 붙이면 동일한 알고리즘 혼동이 재현됩니다(GHSA-ww5h-9m49-7xx4, 2026-07-28 공개, fast-jwt@6.2.2에서 검증). 즉 이 취약점은 3.3.2 → 6.2.0 → 6.3.0, 세 번째 패치에야 완전히 닫혔습니다. §영향받는 버전 표 기준입니다.

08왜 이게 반복되는가

이 CVE가 흥미로운 이유는 새로운 취약점 클래스를 보여줘서가 아니라, 패치 완전성 평가가 왜 필요한지를 그대로 실증해서입니다. 2023년 패치는 신고된 PoC를 정확히 막았습니다. 하지만 "그 PoC가 성립하지 않기 위한 일반 원칙"(입력을 정규화한 뒤 판별한다)을 코드베이스 전체에 강제하지는 못했고, 그 틈이 약 2년 4개월 뒤 같은 결과로 재현됐습니다.

여기서 얻는 방어 원칙은 둘입니다. 첫째, 쌍둥이 함수가 있다면 한쪽에 적용한 정규화·검증 규칙은 반드시 다른 쪽에도 대칭적으로 적용돼야 합니다. 코드 리뷰에서 "이 함수와 구조가 비슷한 함수가 또 있나?"를 묻는 습관이 이런 비대칭을 미리 잡습니다.

둘째, 알고리즘 혼동류 취약점은 검증기가 서명 스킴을 키 자체가 아니라 입력이 자칭하는 값에서 유추하는 설계에서 반복됩니다. 이번 사례는 alg 헤더가 아니라 "키 문자열의 형태"가 그 자칭 값 역할을 했다는 점이 다를 뿐, 뿌리는 같습니다.

이 논지는 이 CVE 안에서 한 번 더 증명됐습니다. 2026년 7월, 이번 재패치(6.2.0~6.2.4)조차 불완전하다는 후속 권고(GHSA-ww5h-9m49-7xx4)가 공개됐습니다. trim()이 걸러낸 것은 ECMAScript 공백 문자뿐이었고, 제어문자나 # 같은 비공백 바이트를 키 앞에 붙이면 같은 알고리즘 혼동이 그대로 재현됐습니다. 첫 패치(3.3.2)도, 두 번째 재패치(6.2.0)도 불완전했고, 진짜 근본 해결은 세 번째 패치인 6.3.0에서야 이뤄졌습니다 — 패치 완전성 평가가 한 번으로 끝나지 않는다는 것을 이 CVE가 스스로 보여준 셈입니다.

패치가 있다는 것과, 그 패치가 문제를 완전히 닫았다는 것은 다른 이야기입니다. 저희가 CVE 심층판에서 항상 "근본원인을 막았는가 / 다른 변형이 남았는가 / silent patch는 아니었는가"를 묻는 이유가 이 사례에 그대로 들어 있습니다.

09결론

결국 이 취약점의 정체는 새로운 버그가 아니라, 약 2년 4개월 전 패치가 덮지 못한 대칭의 빈틈이었습니다. 게다가 이번 재패치(6.2.0)조차 완전하지 않아 또 한 번의 재패치(6.3.0)를 거쳐야 했습니다. 같은 클래스의 코드가 형제 함수로 존재한다면, 한쪽에 적용한 정규화 규칙은 반드시 다른 쪽도 감사해야 한다는 것이 이번 사례가 남기는 교훈입니다.

참고 자료

이 글은 GitHub Security Advisory GHSA-mvf2-f6gm-w987(CVE-2026-34950)과 fast-jwt 오픈소스 저장소(MIT)의 실제 커밋(v6.1.0 → v6.2.0, 태그 커밋 986a77a → e07b1fd)을 저희가 직접 diff로 대조해 재구성한 연구 글입니다. §7·§9의 코드 발췌는 저장소 원문 그대로이며 라인번호까지 직접 확인했습니다. 공격 시나리오(§8)는 권고문에 실린 PoC의 구조를 개념적으로 재서술한 것으로, 완성형 공격 스크립트나 실행 가능한 페이로드는 싣지 않았습니다. 2026년 4월 공개된 최신 CVE라 커뮤니티 2차 분석이 아직 많지 않습니다 — 실제 악용 사례나 다른 JWT 라이브러리로의 동일 변형 여부는 공개 정보로 확인되지 않아 미확인으로 남깁니다. 초고 작성 이후 공개된 후속 권고(GHSA-ww5h-9m49-7xx4, 2026-07-28)를 반영해, 6.2.0~6.2.4도 여전히 재취약하며 실제 안전 버전은 6.3.0이라는 점을 본문·remediation에 정정했습니다.