콘텐츠로 건너뛰기

JWT 보안 모범 사례|alg none 공격 / 만료 / 서명 검증

카테고리: 인증·보안
이 기사는 현재 일본어로만 제공됩니다. 번역본은 순차적으로 공개될 예정입니다.

JWT (JSON Web Token)는 최신 Web API 인증의 사실상의 표준이지만, <strong>명세를 잘못 이해한 채 구현하면 쉽게 취약점을 만들 수 있습니다</strong>. 이 글에서는 실제로 많이 보이는 JWT 오용 패턴과 이를 피하기 위한 모범 사례를 설명합니다.

JWT 구조 복습

JWT는 3개의 Base64URL 인코딩된 문자열을 점으로 연결한 형식입니다.

eyJhbGci...    .   eyJzdWIi...     .    SflKxwRJSM...
   header              payload                signature
  • <strong>header</strong>: 서명 알고리즘(<code>alg</code>)과 토큰 종류(<code>typ</code>)
  • <strong>payload</strong>: 클레임(sub / iss / aud / exp / iat / ...)의 JSON 객체
  • <strong>signature</strong>: header와 payload를 대상으로 한 HMAC / RSA / ECDSA 서명

<a href="/ja/tools/jwt/">DevLab의 JWT 디코더</a>로 JWT의 내용을 확인할 수 있습니다. Base64URL은 인코딩이지 <strong>암호화가 아니므로</strong>, 페이로드의 내용은 서버에서 서명 검증 없이도 누구나 읽을 수 있다는 것을 잊지 마세요.

위협 1: alg=none 공격

JWT 명세에는 <code>"alg": "none"</code>이라는 "서명 없음" 알고리즘 지정이 있습니다. 공격자가 이를 악용하여 <code>{"alg":"none"}</code> 헤더로 페이로드를 수정하고 라이브러리가 그대로 받아들이면 <strong>임의의 사용자로 사칭할 수 있는</strong> 치명적인 취약점이 됩니다.

<strong>대책:</strong>

  • 검증 시 허용할 알고리즘을 <strong>명시적으로 화이트리스트</strong> 지정
  • <code>jwt.verify(token, secret, { algorithms: ['HS256'] })</code>처럼 배열로 전달하기
  • 2015년경의 라이브러리는 취약했지만 현재의 주요 라이브러리는 대책이 마련되어 있습니다. 다만 직접 구현하지 않는 한 최신 버전을 사용해야 합니다.
// ✗ 悪い例 (アルゴリズム未指定 = ライブラリが alg ヘッダを信頼)
jwt.verify(token, secret);

// ✓ 良い例 (アルゴリズムを固定)
jwt.verify(token, secret, { algorithms: ['HS256'] });

위협 2: 키 혼동 공격 (key confusion)

RSA 공개 키를 가진 공격자가 <strong>알고리즘을 RS256에서 HS256으로 변경</strong>하여 공개 키를 "비밀 키"로 처리하게 하는 공격입니다. JWT 라이브러리가 <code>alg</code>를 신뢰하고 검증 함수를 선택하면 공개 키로 HMAC 서명된 위조 토큰이 통과합니다.

<strong>대책:</strong> 알고리즘은 반드시 검증 측에서 고정하세요(위협 1과 동일한 대책). 또한 키 유형도 명시적으로 구분하세요:

  • HS256인 경우 <code>Buffer</code>로 전달
  • RS256의 경우 PEM 형식의 공개 키를 전달합니다.

위협 3: 유효기간 미검증 / 무기한 토큰

JWT의 <code>exp</code> 클레임은 UNIX 초 단위의 만료 시간을 나타내지만, <strong>검증 중에 무시되는 경우가 많습니다</strong>. 한 번 발급된 토큰을 무기한 사용할 수 있다면 유출 시 피해를 막을 수 없습니다.

<strong>대책:</strong>

  • 발급 시 <code>exp</code>를 짧게 설정 (액세스 토큰은 15분~1시간이 기준)
  • 검증할 때 반드시 <code>exp</code> 확인 (주요 라이브러리는 자동으로 수행)
  • 장기 세션이 필요한 경우 리프레시 토큰 패턴 사용 (단기 액세스 토큰 + 장기 리프레시 토큰 + 서버 측 취소 목록)

위협 4: JWT에 민감 정보를 포함

JWT의 페이로드는 단지 Base64 인코딩일 뿐이며 누구나 디코딩할 수 있습니다. 이를 모르고 <strong>페이로드에 암호나 신용 카드 번호를 넣는</strong> 구현이 계속 나타나고 있습니다.

<strong>대책:</strong>

  • payload에는 「이 사용자이다」라는 최소한의 식별 정보만 포함 (sub / user_id / role)
  • 민감한 정보는 서버 측 데이터베이스에 저장하고 JWT에서 user_id로 조회
  • 반드시 JWT로 민감한 정보를 전송해야 하는 경우 <strong>JWE</strong> (암호화된 JWT)를 사용하세요

위협 5: 실효 (logout)가 불가능

JWT를 무상태로 발급하면 취소할 수 없습니다. 사용자가 로그아웃해도 서버에 토큰 정보가 없으므로 "만료될 때까지 사용할 수 있습니다". 비밀번호를 변경해도 발급된 JWT는 계속 유효합니다.

<strong>대책:</strong>

  • 짧은 <code>exp</code> (15분 이하)로 피해 윈도우 최소화
  • 로그아웃 시 서버 측의 <strong>실효 목록 (denylist)</strong>에 <code>jti</code> (JWT ID)를 등록하고 검증 시 대조
  • 중대 이벤트(비밀번호 변경, 권한 변경) 시에는 <code>token_version</code>을 사용자 레코드에 저장하고 검증 시 일치 여부 확인

위협 6: 약한 시크릿

HS256 시크릿이 너무 짧으면 무차별 대입 공격으로 해독될 수 있습니다. 특히 <code>"secret"</code> <code>"password123"</code> 같은 문자열은 즉각적으로 위험합니다.

<strong>대책:</strong>

  • HS256은 최소한 <strong>256비트 (32바이트)</strong>의 무작위 값을 사용해야 함
  • <code>openssl rand -base64 32</code> 또는 <a href="/ja/tools/password/">비밀번호 생성 도구</a>로 생성
  • 시크릿은 절대 Git에 커밋하지 마세요. 환경변수·Secrets Manager로 관리

모범 사례 요약

  1. 검증 측에서 알고리즘 고정 (<code>algorithms: ['HS256']</code>)
  2. HS256 시크릿은 256비트 이상, RS256은 2048비트 이상의 RSA
  3. 액세스 토큰의 <code>exp</code>는 15분~1시간
  4. 리프레시 토큰 패턴으로 장기 세션 구현
  5. payload에 기밀 정보를 포함하지 않음 (Base64는 암호화가 아님)
  6. 로그아웃 시 실효 목록에 <code>jti</code> 등록
  7. 시크릿은 환경변수·Secrets Manager로 관리, 커밋 금지
  8. 클라이언트 측에서 JWT를 localStorage가 아닌 HttpOnly Cookie에 저장 (XSS 대책)

디버깅에 유용한 도구

  • <a href="/ja/tools/jwt/">JWT 디코더</a>: header / payload / signature를 시각화하여 각 claim의 의미를 설명과 함께 표시
  • <a href="/ja/tools/jwt-sign/">JWT 생성・서명 도구</a>: HS256 / HS384 / HS512로 Web Crypto API를 사용하여 브라우저 내에서 서명합니다. 테스트용 토큰 생성에 편리합니다
  • <a href="/ja/tools/password/">비밀번호 생성 도구</a>: 시크릿 생성에 사용할 수 있습니다

요약

JWT는 올바르게 사용하면 편리한 인증 토큰이지만, 명세의 함정과 공격 패턴을 모르면 운영 시스템에 취약점을 심을 위험이 있습니다. 이 글에서 제시한 6가지 위협과 대책을 파악하고 JWT 라이브러리의 최신 버전을 사용하는 것이 기본입니다. 정기적으로 구현을 검토하고 보안 공지를 추적할 것을 권장합니다.