최신 웹 서비스에서 인증과 인가는 사용자의 신원을 확인하고 접근 권한을 제어하는 핵심적인 과정입니다. 이 과정에서 JWT(JSON Web Token)는 그 간편함과 효율성으로 인해 널리 사용되고 있습니다. 스테이트리스(Stateless)한 특성과 다양한 플랫폼 간의 호환성 덕분에 많은 개발자가 JWT를 선호하지만, 과연 여러분의 JWT 구현은 안전하다고 확신할 수 있을까요? 편리함 뒤에 숨겨진 보안 취약점을 간과한다면, 서비스 전체가 심각한 위협에 노출될 수 있습니다.
이 글에서는 JWT의 기본적인 동작 방식을 이해하고, 실제 서비스에서 발생할 수 있는 주요 보안 취약점들을 심층적으로 분석합니다. 나아가, 이러한 취약점들을 방어하고 안전한 JWT 구현을 위한 구체적이고 실용적인 전략들을 제시하여 여러분의 웹 서비스를 더욱 견고하게 보호할 수 있도록 돕겠습니다.
📑 목차
- JWT(JSON Web Token)란 무엇이며, 왜 중요한가?
- JWT의 기본 구조와 동작 방식
- JWT의 주요 보안 취약점 심층 분석
- 서명(Signature) 관련 취약점
- 페이로드(Payload) 및 기타 취약점
- 서명(Signature) 취약점 방어 및 안전한 서명 구현 전략
- alg=none 취약점 방어
- 강력한 비밀 키(Secret Key) 사용 및 관리
- 올바른 알고리즘 선택 및 구현
- 페이로드 및 토큰 관리 취약점 방어 전략
- 페이로드에 민감 정보 포함 금지
- 안전한 토큰 저장 및 전송
- 토큰 만료 및 무효화 관리
- JWT 보안 강화를 위한 추가적인 모범 사례
- 결론: 안전한 JWT 구현을 위한 지속적인 노력
Image by Lalmch on Pixabay
JWT(JSON Web Token)란 무엇이며, 왜 중요한가?
JWT(JSON Web Token)는 웹 환경에서 정보를 안전하게 주고받기 위해 정의된 토큰 기반의 인증 및 인가 메커니즘입니다. 클라이언트와 서버 간의 통신 시, 사용자 정보를 포함하는 JSON 객체를 암호화(정확히는 서명)하여 토큰 형태로 전달함으로써 매 요청마다 사용자 정보를 다시 조회할 필요 없이 상태를 유지할 수 있게 합니다.
JWT의 기본 구조와 동작 방식
JWT는 Header.Payload.Signature 세 부분으로 구성되며, 각 부분은 Base64Url로 인코딩되어 점(.)으로 연결됩니다.
- Header (헤더): 토큰의 타입(typ)과 서명에 사용된 알고리즘(alg) 정보를 포함합니다. 예를 들어,
{"alg": "HS256", "typ": "JWT"}와 같습니다. - Payload (페이로드): 토큰에 담을 실제 정보인 클레임(Claim)을 포함합니다. 사용자 ID, 권한 정보, 토큰 만료 시간 등 필요한 데이터를 JSON 형태로 담습니다. 예를 들어,
{"sub": "1234567890", "name": "John Doe", "iat": 1516239022}와 같습니다. - Signature (서명): 인코딩된 헤더와 페이로드, 그리고 서버의 비밀 키(Secret Key)를 사용하여 생성된 값입니다. 이 서명은 토큰의 무결성(Integrity)을 보장하며, 토큰이 위변조되었는지 확인할 때 사용됩니다.
JWT의 가장 큰 장점은 스테이트리스(Stateless)입니다. 서버가 클라이언트의 세션 상태를 저장할 필요가 없어 서버 확장이 용이하고, 마이크로서비스 아키텍처나 모바일 환경에서 유용하게 활용됩니다. 그러나 이러한 장점은 동시에 적절한 보안 전략 없이는 심각한 취약점으로 이어질 수 있습니다.
JWT의 주요 보안 취약점 심층 분석
JWT는 잘 설계된 표준이지만, 구현상의 오류나 오용으로 인해 다양한 보안 취약점에 노출될 수 있습니다. 각각의 취약점들을 자세히 살펴보겠습니다.
서명(Signature) 관련 취약점
alg=none취약점: JWT 헤더의alg필드에none을 설정하면, 토큰 생성 시 서명 검증을 생략하겠다는 의미가 됩니다. 만약 서버가 이none알고리즘을 제대로 처리하지 못하고 서명 검증 없이 토큰을 수락한다면, 공격자는 임의의 페이로드를 담은 토큰을 만들어alg=none으로 설정하여 서버의 인증 시스템을 우회할 수 있습니다. 이는 가장 치명적인 JWT 취약점 중 하나로 꼽힙니다.// 공격자가 조작한 JWT 헤더 예시 { "alg": "none", "typ": "JWT" }- 약한 비밀 키(Weak Secret Key) 사용: HMAC 알고리즘을 사용할 때, 서버에서 토큰 서명에 사용되는 비밀 키가 너무 짧거나 예측하기 쉽다면, 공격자는 무차별 대입(Brute-force) 공격이나 딕셔너리 공격을 통해 비밀 키를 알아낼 수 있습니다. 비밀 키가 노출되면 공격자는 유효한 서명을 가진 토큰을 위조하여 인증 시스템을 완전히 장악할 수 있게 됩니다. 실제 공격 사례에서 수십억 건의 비밀 키 조합을 테스트하여 약한 키를 찾아낸 경우가 보고된 바 있습니다.
- 비대칭 알고리즘 오용 (RSA -> HMAC): RSA와 같은 비대칭 알고리즘은 공개 키와 개인 키를 사용합니다. 서버는 개인 키로 토큰을 서명하고, 클라이언트는 공개 키로 서명을 검증합니다. 이때 공격자가
alg필드를RS256에서HS256(대칭 알고리즘)으로 변경하고, 서버의 공개 키를 HMAC의 비밀 키로 사용하도록 유도한다면, 공개 키만으로도 유효한 토큰을 위조할 수 있습니다. 이는 서버가alg필드의 변경을 제대로 검증하지 않을 때 발생합니다.
페이로드(Payload) 및 기타 취약점
- 민감 정보 노출: JWT의 페이로드는 단순히 Base64Url로 인코딩될 뿐, 기본적으로 암호화되지 않습니다. 따라서 토큰이 탈취되면 페이로드에 포함된 모든 정보가 그대로 노출됩니다. 주민등록번호, 비밀번호, 금융 정보 등 민감한 개인 정보를 페이로드에 직접 포함하는 것은 매우 위험합니다.
- 토큰 탈취(XSS, CSRF): JWT 토큰이 클라이언트 측(예: Local Storage)에 저장될 경우, XSS(Cross-Site Scripting) 공격에 의해 토큰이 탈취될 수 있습니다. 탈취된 토큰은 공격자가 사용자 권한으로 서비스에 접근하는 데 사용될 수 있습니다. 또한, CSRF(Cross-Site Request Forgery) 공격에 취약할 수 있으며, 특히 쿠키에 저장된 토큰의 경우 주의가 필요합니다.
- 만료 기간 관리 부재: 토큰에 적절한 만료 기간(Expiration Time)을 설정하지 않거나 너무 길게 설정하면, 탈취된 토큰이 장기간 유효하여 공격에 사용될 위험이 커집니다. 반대로 너무 짧게 설정하면 사용자 경험을 저해할 수 있습니다. 리플레이 공격(Replay Attack)에도 노출될 수 있습니다.
- 토큰 무효화 문제: JWT는 스테이트리스 특성 때문에 발행된 토큰을 서버에서 직접적으로 무효화하기 어렵습니다. 사용자가 로그아웃하거나 비밀번호를 변경했을 때, 탈취된 토큰이 여전히 유효할 수 있다는 문제가 발생합니다. 이는 세션 기반 인증과 비교했을 때 JWT의 주요 단점 중 하나입니다.
서명(Signature) 취약점 방어 및 안전한 서명 구현 전략
JWT의 서명은 토큰의 무결성을 보장하는 핵심 요소입니다. 서명 관련 취약점을 방어하기 위한 구체적인 전략들을 살펴보겠습니다.
alg=none 취약점 방어
가장 먼저, 서버 측에서는 JWT 라이브러리를 통해 토큰을 검증할 때 alg=none 알고리즘을 명시적으로 거부해야 합니다. 대부분의 JWT 라이브러리는 alg=none을 기본적으로 허용하지 않지만, 경우에 따라 명시적으로 안전한 알고리즘만을 허용하도록 설정해야 합니다. 예를 들어, 허용할 알고리즘 목록(예: HS256, RS256)을 지정하는 방식으로 구현합니다.
강력한 비밀 키(Secret Key) 사용 및 관리
HMAC 알고리즘을 사용하는 경우, 비밀 키는 최소 32바이트(256비트) 이상의 길이를 가진 무작위 문자열로 생성해야 합니다. 예측 불가능하고 충분히 긴 키를 사용함으로써 무차별 대입 공격에 대한 저항력을 높입니다. 비밀 키는 코드 내에 하드코딩하지 않고, 환경 변수, 설정 파일, 또는 AWS KMS, Azure Key Vault와 같은 키 관리 시스템(KMS)을 사용하여 안전하게 보관하고 접근을 제어해야 합니다. 또한, 주기적으로 키 갱신(Key Rotation) 정책을 수립하여 비밀 키가 노출되었을 때의 피해 범위를 최소화해야 합니다.
올바른 알고리즘 선택 및 구현
어떤 서명 알고리즘을 사용할지는 서비스의 특성과 보안 요구사항에 따라 달라집니다. HMAC(HS256, HS384, HS512)는 대칭 키 암호화 방식으로, 하나의 비밀 키로 서명 생성 및 검증을 모두 수행합니다. 반면 RSA(RS256, RS384, RS512)나 ECDSA(ES256, ES384, ES512)는 비대칭 키 암호화 방식으로, 개인 키로 서명하고 공개 키로 검증합니다.
| 특징 | HMAC (대칭 키) | RSA / ECDSA (비대칭 키) |
|---|---|---|
| 키 종류 | 단일 비밀 키 | 개인 키(서명), 공개 키(검증) |
| 키 관리 복잡성 | 상대적으로 단순 (하나의 키만 관리) | 복잡 (개인 키 안전 보관, 공개 키 배포) |
| 주요 사용처 | 단일 서버 또는 소규모 분산 환경 | 여러 서비스가 JWT를 발행하고 검증하는 대규모 분산 환경 (예: 마이크로서비스) |
| 보안 고려사항 | 비밀 키 노출 시 전체 시스템 위험 | 개인 키 노출 시 위험. 공개 키는 외부에 공개 가능 |
비대칭 알고리즘을 사용할 때는 공격자가 헤더의 alg 필드를 RS256에서 HS256으로 변경하는 공격에 대비하여, 서버에서 공개 키를 HMAC 비밀 키로 오용하지 않도록 알고리즘 검증 로직을 철저히 구현해야 합니다. 즉, 토큰 헤더에 명시된 알고리즘과 서버가 실제로 사용하는 알고리즘이 일치하는지 확인해야 합니다.
Image by Tumisu on Pixabay
페이로드 및 토큰 관리 취약점 방어 전략
JWT의 페이로드는 사용자 정보가 담기는 곳이며, 클라이언트 측에 저장되는 토큰 자체의 관리 방식 또한 중요한 보안 고려사항입니다. 이와 관련된 취약점을 방어하는 전략을 알아보겠습니다.
페이로드에 민감 정보 포함 금지
가장 중요한 원칙은 페이로드에 민감한 정보를 절대 포함하지 않는 것입니다. 사용자 ID(sub 클레임 등)나 역할(role 클레임)과 같이 최소한의 식별 정보 및 인가에 필요한 정보만 포함해야 합니다. 만약 추가적인 사용자 정보(예: 이메일, 전화번호)가 필요하다면, 페이로드의 사용자 ID를 이용해 백엔드에서 데이터베이스를 조회하여 가져와야 합니다. JWE(JSON Web Encryption)를 사용하여 페이로드 자체를 암호화할 수도 있지만, 이는 구현의 복잡성을 크게 증가시키므로 신중하게 고려해야 합니다.
안전한 토큰 저장 및 전송
JWT 토큰을 클라이언트에 저장하는 방식은 XSS 및 CSRF 공격 방어에 핵심적인 영향을 미칩니다.
- HTTP Only Cookie 사용: JWT를 HTTP Only Cookie에 저장하는 것이 XSS 공격 방어에 가장 효과적입니다. HTTP Only 속성이 설정된 쿠키는 JavaScript를 통해 접근할 수 없으므로, 공격자가 XSS를 통해 악성 스크립트를 주입하더라도 토큰을 탈취하기 어렵습니다.
- SameSite 속성 적용: 쿠키에 SameSite 속성을
Lax또는Strict로 설정하여 CSRF 공격을 방어할 수 있습니다.SameSite=Lax는 대부분의 CSRF 공격을 막으면서도 사용자 경험을 크게 해치지 않는 좋은 기본값입니다. - HTTPS 필수 적용: JWT를 포함한 모든 통신은 반드시 HTTPS를 통해 이루어져야 합니다. HTTPS는 전송 중인 데이터를 암호화하여 중간자 공격(Man-in-the-Middle Attack)으로부터 토큰이 탈취되는 것을 방지합니다.
| 저장 방식 | 장점 | 단점 및 보안 고려사항 |
|---|---|---|
| Local Storage / Session Storage | JavaScript로 접근 용이, CORS 문제 없음 | XSS 취약 (JavaScript로 쉽게 탈취 가능) |
| HTTP Only Cookie | XSS 방어에 효과적 (JavaScript 접근 불가) | CSRF 취약 (SameSite 속성으로 방어 필요) CORS 환경에서 다소 복잡할 수 있음 |
토큰 만료 및 무효화 관리
JWT의 만료 기간은 보안과 사용성 사이의 균형을 맞추는 것이 중요합니다.
- 짧은 만료 기간 설정 및 Refresh Token 활용: Access Token의 만료 기간은 매우 짧게(예: 5~15분) 설정하여 토큰이 탈취되더라도 공격자가 사용할 수 있는 시간을 최소화해야 합니다. 대신, Access Token이 만료되면 새로운 Access Token을 발급받기 위한 Refresh Token을 사용합니다. Refresh Token은 Access Token보다 긴 만료 기간(예: 1일~수 주)을 가지며, 데이터베이스에 저장하여 사용 시마다 유효성을 검증하고 재사용 방지 로직을 구현해야 합니다.
- Refresh Token의 안전한 관리: Refresh Token은 Access Token보다 훨씬 민감한 정보이므로, 더욱 안전하게 관리해야 합니다. HTTP Only Cookie에 저장하고, 서버 측에서는 Refresh Token을 발급할 때마다 유니크한 값을 생성하고 데이터베이스에 저장하여 관리합니다. 사용자가 로그아웃하거나 비밀번호를 변경할 경우, 해당 Refresh Token을 데이터베이스에서 삭제하거나 블랙리스트에 추가하여 무효화해야 합니다.
- 블랙리스트/화이트리스트: 스테이트리스 특성상 JWT는 즉각적인 무효화가 어렵지만, 특정 상황(로그아웃, 비밀번호 변경, 탈취 의심)에서는 강제로 토큰을 무효화해야 합니다. 이를 위해 서버에서 무효화된 토큰 목록(블랙리스트)을 유지하거나, 유효한 토큰 목록(화이트리스트)을 관리하는 방식을 고려할 수 있습니다. 블랙리스트는 Redis와 같은 인메모리 데이터베이스를 활용하여 빠른 조회를 가능하게 하는 것이 일반적입니다.
Image by RuslanSikunov on Pixabay
JWT 보안 강화를 위한 추가적인 모범 사례
앞서 언급된 전략 외에도, JWT 기반 시스템의 전반적인 보안 수준을 높이기 위한 여러 가지 모범 사례가 있습니다.
- 검증된 JWT 라이브러리 사용: 직접 JWT를 구현하는 것은 매우 위험합니다. 보안 취약점을 내포할 가능성이 크기 때문입니다. 항상 검증되고 널리 사용되며 꾸준히 업데이트되는 JWT 라이브러리(예: Node.js의
jsonwebtoken, Python의PyJWT, Java의Auth0 JWT)를 사용하는 것이 좋습니다. 이러한 라이브러리들은alg=none과 같은 알려진 취약점에 대한 방어 로직을 내장하고 있습니다. - 모든 클레임(Claim) 유효성 검증: 토큰 페이로드의 클레임들은 클라이언트가 조작할 수 있는 여지가 있습니다. 따라서 서버는 토큰을 받은 후,
exp(만료 시간),nbf(유효 시작 시간),iss(발행자),aud(수신자)와 같은 표준 클레임은 물론, 서비스에 특화된 사용자 정의 클레임(예:role)도 철저히 검증해야 합니다. 예를 들어, 사용자가 관리자 권한을 요청하더라도 토큰의role클레임이 'user'라면, 서버는 관리자 권한을 부여해서는 안 됩니다.// 클레임 검증 예시 (Node.js jsonwebtoken 라이브러리 활용) try { const decoded = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'], // 허용할 알고리즘 명시 maxAge: '15m', // Access Token 최대 만료 시간 검증 issuer: 'your-service', // 발행자 검증 audience: 'your-app' // 수신자 검증 }); // decoded.role === 'admin' 등 사용자 정의 클레임 추가 검증 } catch (err) { // 토큰 만료, 서명 불일치 등 오류 처리 } - 상세한 오류 메시지 노출 방지: JWT 검증 실패 시, 클라이언트에 너무 상세한 오류 메시지(예: '비밀 키가 잘못되었습니다', '알고리즘이 일치하지 않습니다')를 반환하지 않도록 주의해야 합니다. 이러한 메시지는 공격자에게 유용한 정보를 제공할 수 있으므로, 일반적인 '유효하지 않은 토큰입니다'와 같은 메시지를 반환하는 것이 좋습니다.
- 모니터링 및 로깅: JWT 인증/인가 과정에서 발생하는 모든 중요한 이벤트(토큰 발급, 검증 성공/실패, 만료, 무효화 시도)를 로깅하고 모니터링해야 합니다. 비정상적인 토큰 사용 시도나 잦은 인증 실패는 공격 시도의 징후일 수 있으므로, 이상 징후를 감지하고 경고를 발생시키는 시스템을 구축하는 것이 중요합니다.
- Rate Limiting 적용: 토큰 발행 및 검증 API에 Rate Limiting(요청 빈도 제한)을 적용하여 무차별 대입 공격이나 서비스 거부(DoS) 공격을 방어해야 합니다. 예를 들어, 로그인 시도 횟수를 제한하거나, Refresh Token 요청 횟수를 제한하는 등의 정책을 적용할 수 있습니다.
결론: 안전한 JWT 구현을 위한 지속적인 노력
JWT(JSON Web Token)는 현대 웹 서비스에서 강력하고 유연한 인증 및 인가 솔루션을 제공합니다. 그러나 그 잠재력을 최대한 활용하면서도 보안을 유지하려면, JWT의 동작 원리와 함께 발생할 수 있는 보안 취약점들을 명확히 이해하고 적절한 방어 전략을 적용하는 것이 필수적입니다.
이 글에서 다룬 alg=none 취약점 방어, 강력한 비밀 키 관리, 페이로드에 민감 정보 포함 금지, HTTP Only Cookie 및 SameSite 속성을 활용한 안전한 토큰 저장, 그리고 Refresh Token을 이용한 토큰 만료 및 무효화 전략 등은 안전한 JWT 구현을 위한 핵심적인 요소들입니다. 이 외에도 검증된 라이브러리 사용, 철저한 클레임 검증, 모니터링 및 Rate Limiting 적용 등 다층적인 보안 접근 방식이 요구됩니다.
JWT 보안은 단순히 한 번의 설정으로 끝나는 것이 아니라, 개발 과정 전반에 걸쳐 보안 의식을 가지고 지속적으로 시스템을 점검하고 업데이트하는 노력이 필요합니다. 여러분의 웹 서비스가 이러한 보안 전략들을 통해 더욱 견고하고 신뢰할 수 있는 시스템으로 거듭나기를 바랍니다.
혹시 여러분의 서비스에서 JWT 보안과 관련하여 겪었던 경험이나 추가적인 팁이 있다면 댓글로 공유해 주세요. 함께 더 안전한 웹 환경을 만들어나가는 데 큰 도움이 될 것입니다.