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로 관리
모범 사례 요약
- 검증 측에서 알고리즘 고정 (<code>algorithms: ['HS256']</code>)
- HS256 시크릿은 256비트 이상, RS256은 2048비트 이상의 RSA
- 액세스 토큰의 <code>exp</code>는 15분~1시간
- 리프레시 토큰 패턴으로 장기 세션 구현
- payload에 기밀 정보를 포함하지 않음 (Base64는 암호화가 아님)
- 로그아웃 시 실효 목록에 <code>jti</code> 등록
- 시크릿은 환경변수·Secrets Manager로 관리, 커밋 금지
- 클라이언트 측에서 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 라이브러리의 최신 버전을 사용하는 것이 기본입니다. 정기적으로 구현을 검토하고 보안 공지를 추적할 것을 권장합니다.