블로그로 돌아가기
Security 2026-04-29

2026년 JWT 보안 모범 사례

가장 흔한 JWT 취약점 방지: 알고리즘 혼동, 약한 시크릿, 리플레이.

JWT는 수백만 시스템에서 인증 상태를 운반하며, 똑같은 몇 가지 구현 실수가 계속해서 완전한 계정 탈취 취약점을 만들어냅니다. JWT 모범 사례 문서인 RFC 8725가 존재하는 이유는 바로 기본 사양(RFC 7519)이 순진하게 쓰기에는 너무 관대하기 때문입니다. 실제로 무엇이 잘못되는지, 각 실패를 어떻게 막는지 살펴봅시다.

알고리즘 혼동은 여전히 1순위 취약점

토큰 헤더는 자신의 알고리즘을 스스로 선언합니다. 그 선언을 신뢰하는 검증기는 공격자에게 열쇠를 넘겨주는 셈입니다. 고전적인 공격 두 가지:

  • alg none. 사양은 "alg": "none"인 무서명 토큰을 허용합니다. 이를 수용하는 라이브러리는 어떤 위조 페이로드든 검증된 것으로 취급합니다. 10년도 넘은 공격인데 오래된 라이브러리를 감싼 새 래퍼에서 계속 재등장합니다.
  • RS256에서 HS256으로의 다운그레이드. RS256 토큰을 검증하는 서비스는 보통 RSA 공개키를 갖고 있습니다. 공격자가 "alg": "HS256" 토큰을 만들어 그 공개키를 HMAC 시크릿으로 서명하면, 헤더의 alg에 따라 분기하는 라이브러리는 이를 성공적으로 검증합니다. 공개키는 정의상 공개되어 있으니까요.

수정은 한 줄이며 선택 사항이 아닙니다.

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

서버 측에서 정확한 알고리즘 목록을 고정하세요. 절대 토큰이 선택하게 두지 마세요.

2026년의 알고리즘 선택

  • HS256 — 대칭 HMAC. 발급자와 검증자가 같은 서비스일 때 적합. 시크릿은 반드시 무작위 256비트 값이어야 합니다: openssl rand -base64 32. 사람이 만든 패스프레이즈는 토큰 위조를 단일 HMAC에 대한 오프라인 사전 공격으로 격하시킵니다.
  • RS256 — RSA 서명. 어디서나 지원되고 검증자는 공개키만 필요하지만, 서명이 크고 RSA 키 생성이 느립니다.
  • ES256 / EdDSA — 타원곡선. 더 작은 토큰, 더 빠른 검증. EdDSA(Ed25519)는 ECDSA 배포를 무너뜨려 온 논스 재사용 함정도 제거합니다. 새 분산 시스템에는 이쪽을 선호하세요.

공개키는 JWKS 엔드포인트로 배포하고, 새 키로 서명하기 전에 먼저 게시하는 방식으로 회전하세요. 의심해야 할 헤더 필드가 둘 있습니다. jkux5u는 토큰이 검증 키를 임의의 URL로 가리키게 합니다. RFC 8725는 단호합니다. 엄격한 허용 목록에 없는 한 무시하세요. 마찬가지로 kid는 불투명한 조회 식별자로만 취급하세요. 이를 파일 경로나 SQL 쿼리에 삽입한 구현들은 양쪽 방식 모두로 악용된 바 있습니다.

서명만이 아니라 모든 클레임을 검증

암호학적으로 유효한 토큰도 여전히 잘못된 토큰일 수 있습니다.

const payload = jwt.verify(token, key, {

algorithms: ['ES256'],

issuer: 'https://auth.example.com',

audience: 'https://api.example.com',

clockTolerance: 5

});

  • iss — 생략하면 다른(어쩌면 공격자가 등록한) 테넌트의 토큰이 통과합니다.
  • aud — 생략하면 한 API용으로 발급된 토큰이 다른 API에 재사용됩니다. 마이크로서비스 군단의 고전적인 크로스 서비스 혼동된 대리인 버그입니다.
  • exp / nbf — 작은 시계 허용 오차(분이 아니라 초 단위)로 강제하세요.
  • typ — RFC 9068은 액세스 토큰에 at+jwt를 정의합니다. 이를 확인하면 ID 토큰이나 리프레시 토큰이 액세스 토큰 자리에서 수용되는 것을 막습니다.

수명, 저장, 폐기는 하나의 설계

이 세 가지 결정은 함께여야만 의미가 있습니다.

  • 액세스 토큰: 5~15분, 메모리에만 보관. localStorage는 절대 금지. 어떤 XSS든 동기적으로 읽어가며, httpOnly 플래그도 거기서는 도움이 안 됩니다.
  • 리프레시 토큰: httpOnly, Secure, SameSite=Strict 쿠키, 회전과 함께: 매 갱신마다 새 토큰을 발급하고 이전 것을 무효화합니다. 회전되어 폐기된 토큰이 다시 제시되면 세션 전체를 폐기하세요. 그 재사용이 바로 도난의 서명입니다.
  • 폐기: 무상태 JWT는 폐기할 수 없습니다. 노출 창이 수용 가능할 만큼 수명을 짧게 유지하고, 로그아웃과 침해 대응을 위해 jti 클레임을 키로 하는 서버 측 거부 목록을 유지하세요. 제품이 어디서나 즉각적이고 확실한 폐기를 요구한다면, 그것이 JWT 대신 세션 저장소 기반 불투명 토큰을 써야 한다는 신호입니다. 전혀 부끄럽지 않은 아키텍처입니다.

페이로드 위생

페이로드는 base64url 인코딩일 뿐 암호화가 아닙니다. 토큰을 쥔 누구든 디코더 호출 한 번으로 모든 클레임을 읽습니다. PII, 비밀로 여기는 역할 정보, 내부 식별자를 넣지 말고 토큰을 작게 유지하세요. 토큰은 모든 요청에 실리며, 쿠키 속 4KB의 클레임은 프록시와 CDN의 헤더 제한을 초과할 수 있습니다.

디버깅 체크리스트

  • 배포 후 서명 무효: JWKS 중첩 없이 키 회전이 일어났습니다.
  • 로컬에서는 유효한데 프로덕션에서 거부: 시계 오차 또는 프록시가 Authorization 헤더를 제거.
  • 정시마다 간헐적 401: 표류하는 VM 시계에 대해 clockTolerance 없이 exp 검증.
  • 디코딩은 되는데 verify 실패: verify 자리에 decode를 호출하고 있습니다. decode는 어떤 암호학적 검사도 하지 않습니다.

JWS, JWE, 그리고 JWT가 실제로 가리키는 것

엄밀히 말해 JWT는 클레임 형식의 이름이고, 컨테이너는 JWS(서명됨, 읽을 수 있음) 또는 JWE(암호화됨)입니다. 실제 세상의 거의 모든 "JWT"는 JWS이며, 페이로드의 어떤 것도 기밀이 아닌 이유가 정확히 이것입니다. 클라이언트에게 숨겨야 하는 클레임이 있다면 — 내부 사용자 플래그, 리스크 점수 — 토큰 뒤 서버 측에 두거나 JWE를 쓰고 추가되는 키 관리 복잡성을 감수하세요. 애초에 보내지 않아도 되는 데이터를 숨기려고 암호화하는 것은 대개 잘못된 거래입니다.

sdk.is/jwt-decoder 의 JWT 디코더로 헤더와 클레임을 안전하게 들여다보세요. 디코딩은 검사일 뿐, 절대 인증이 아닙니다.