개발 이야기3분 읽기

JWT 검증이 통과하는데도 뚫리는 자리

alg를 토큰에서 읽으면 공개키로 서명한 위조 토큰이 통과합니다. 실제로 재현해 봤습니다.

JWT 검증은 서명을 확인하는 일입니다. 그 확인이 성공했는데도 위조 토큰이 지나가는 경우가 있습니다.

alg를 토큰에서 읽으면 안 된다

JWT 헤더에 알고리즘이 적혀 있습니다.

{ "alg": "RS256", "typ": "JWT" }

이 값을 읽어서 검증 방식을 정하는 구현이 있습니다. 문제는 헤더가 토큰의 일부이고, 토큰은 공격자가 만든다는 점입니다.

alg: none

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ1c2VyLTEiLCJyb2xlIjoiYW…

세 번째 조각이 비어 있습니다. 서명이 없다는 뜻이고, 규격상 유효한 토큰 형식입니다(RFC 7519의 "Unsecured JWS").

헤더의 alg를 그대로 믿는 검증기는 "서명 없음"으로 분기해서 통과시킵니다.

RS256을 HS256으로 바꿔치기

이쪽이 더 미묘합니다. RSA 공개키로 검증하도록 만든 시스템에서, 토큰의 algHS256으로 바꾸고 공개키를 HMAC 비밀키로 써서 서명합니다.

실제로 돌려 봤습니다.

정상 RS256 토큰: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c…
위조 HS256 토큰: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c…
취약한 검증기에 위조 토큰을 넣으면: 통과

검증기가 alg를 보고 "HS256이네" 하고 HMAC 경로로 가면서, 갖고 있는 키(공개키)를 HMAC 비밀키로 씁니다. 공격자도 같은 공개키를 갖고 있으므로 같은 서명을 만들 수 있습니다.

공개키는 공개되어 있습니다. 그것이 비밀키 자리에 들어가면 비밀이 아닙니다.

고치는 방법

알고리즘은 토큰이 아니라 서버가 정합니다.

jwt.verify(token, key, { algorithms: ['RS256'] })

허용 목록을 명시하면 헤더에 무엇이 적혀 있든 그 방식으로만 검증합니다. 대부분의 라이브러리가 이 옵션을 갖고 있고, 최근 버전은 대체로 필수로 요구합니다.

서명이 유효해도 만료는 별개다

서명이 유효한가: true
exp 가 지났나 : true (2026-08-29T13:57:14.000Z)

하루 지난 토큰입니다. 서명은 완전히 유효합니다 — 만료된 토큰의 서명이 무효가 되는 것이 아니라, 그냥 exp 클레임이 과거일 뿐입니다.

서명 검증과 클레임 검증은 다른 단계입니다. 라이브러리의 verifyexp까지 보는지 확인해야 하고, 보지 않는다면 직접 검사해야 합니다.

nbf(not before)와 iss·aud도 같습니다. 다른 서비스용으로 발급된 유효한 토큰이 aud 검사 없이 통과할 수 있습니다.

페이로드는 암호화가 아니다

{"sub":"user-1","role":"admin","exp":1788101834}

토큰의 두 번째 조각을 base64url 디코딩한 결과입니다. 키가 필요 없습니다.

서명은 변조를 막을 뿐 내용을 가리지 않습니다. 토큰에 내부 식별자나 권한 구조를 담으면 그대로 읽힙니다. 감춰야 할 값이 있으면 JWE를 쓰거나 토큰에 넣지 않습니다.

kid를 신뢰하면

kid(key id) 헤더로 어느 키를 쓸지 고르는 구성이 흔합니다. 이 값도 토큰의 일부입니다.

kid를 파일 경로나 SQL 조회에 그대로 넣으면 경로 탐색이나 주입이 열립니다. kid는 미리 등록된 키 목록의 조회용으로만 쓰고, 목록에 없으면 거부해야 합니다.

확인 순서

  1. verify 호출에 알고리즘 허용 목록이 있는지 봅니다. 없으면 추가합니다
  2. HMAC과 RSA를 함께 쓰는 시스템이면 키가 용도별로 분리돼 있는지 확인합니다
  3. exp·nbf·aud·iss를 검사하는지 봅니다. 서명 검증과 별개입니다
  4. 토큰 페이로드에 감춰야 할 값이 없는지 봅니다. 누구나 읽습니다
  5. kid를 파일 경로나 쿼리에 그대로 넣지 않습니다

tools.onuel.devJWT 생성기는 HS·RS·PS·ES·EdDSA로 토큰을 서명하고, alg: none 토큰도 만들어 검증기가 어떻게 반응하는지 확인할 수 있습니다. JWT 검증기는 서명이 유효한지와 유효 기간 안인지를 함께 봅니다. 내용만 열어 볼 때는 JWT 디코더를 쓰고, 서명 원리는 HMACRSA 서명에서 각각 볼 수 있습니다.

  • #JWT
  • #인증
  • #보안