JWT(JSON Web Token)는 편리하지만 여러 보안 취약점을 가질 수 있습니다. 이 글에서는 JWT의 주요 취약점을 분석하고, 이를 방어하기 위한 안전하고 실용적인 구현 전략을 제시합니다.
웹 애플리케이션 개발에서 사용자 인증은 필수적인 요소입니다. 수많은 인증 방식 중 JWT(JSON Web Token)는 그 간결함과 확장성 덕분에 RESTful API 및 마이크로서비스 아키텍처에서 널리 사용되고 있습니다. Stateless(무상태) 특성으로 서버 자원을 효율적으로 사용하고, 다양한 플랫폼에서 쉽게 통합할 수 있다는 장점은 개발자들에게 큰 매력으로 다가옵니다. 하지만, 이러한 편리함 뒤에는 간과해서는 안 될 보안 취약점들이 존재합니다. 과연 여러분의 JWT 구현은 안전하다고 확신할 수 있을까요? 편리함에 가려져 있던 잠재적인 위험 요소들을 파악하고, 견고한 시스템을 구축하기 위한 실질적인 방안을 함께 모색해 봅시다.
📑 목차
Image by Lalmch on Pixabay
JWT, 왜 중요하지만 위험할까?
JWT는 클라이언트와 서버 간의 정보를 안전하게 전송하기 위한 compact하고 URL-safe한 방법으로 정의됩니다. 주로 사용자 인증 및 권한 부여에 사용되며, 한 번 발급된 토큰은 클라이언트 측에 저장되었다가 요청 시 서버로 전송되어 사용자를 식별하고 권한을 확인하는 데 쓰입니다. 이는 서버가 세션 상태를 유지할 필요가 없어 확장성이 뛰어나다는 장점을 제공합니다.
JWT는 일반적으로 Header, Payload, Signature의 세 부분으로 구성됩니다.
- Header (헤더): 토큰의 타입(typ)과 서명에 사용된 알고리즘(alg)을 명시합니다. 예:
{"alg": "HS256", "typ": "JWT"} - Payload (페이로드): 클레임(Claim)이라고 불리는 실제 정보가 담겨 있습니다. 사용자 ID, 만료 시간, 권한 등 필요한 데이터가 여기에 포함됩니다. 예:
{"sub": "1234567890", "name": "John Doe", "iat": 1516239022} - Signature (서명): 인코딩된 헤더와 인코딩된 페이로드, 그리고 서버에만 알려진 비밀 키(Secret Key)를 사용하여 생성됩니다. 이 서명은 토큰의 무결성을 검증하는 데 사용됩니다. 즉, 토큰이 중간에 변조되지 않았음을 보장합니다.
이 세 부분이 Base64Url로 인코딩된 후 .으로 연결되어 최종 JWT 문자열이 만들어집니다. 문제는 이 과정에서 발생할 수 있는 여러 가지 취약점입니다. 데이터가 인코딩될 뿐 암호화되는 것이 아니므로 페이로드에 민감한 정보가 포함되면 쉽게 노출될 수 있고, 서명 검증 과정이나 토큰 관리 방식에 따라 다양한 공격에 노출될 수 있습니다.
JWT의 대표적인 보안 취약점 분석
JWT의 구조적 특성과 구현 방식에 따라 여러 가지 보안 취약점이 발생할 수 있습니다. 주요 취약점들을 깊이 있게 살펴보겠습니다.
1. 서명(Signature) 관련 취약점
JWT의 핵심 보안 기능은 서명입니다. 이 서명이 제대로 작동하지 않거나 우회될 경우 토큰의 무결성이 손상될 수 있습니다.
None 알고리즘 취약점
JWT 헤더의 "alg" 필드에 "none" 값을 설정하면 서명 검증을 하지 않도록 서버에 지시할 수 있습니다. 만약 서버가 이 "none" 알고리즘을 제대로 처리하지 않고 서명 없이도 토큰을 유효하다고 판단한다면, 공격자는 페이로드를 임의로 조작하여 권한을 탈취하거나 다른 사용자 행세를 할 수 있습니다. 예를 들어, "admin": true와 같은 클레임을 추가하여 관리자 권한을 얻으려 시도할 수 있습니다.
// 공격자가 조작한 JWT (서명 없음)
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VySWQiOiJhZG1pbiIsImlzQWRtaW4iOnRydWV9.
약한 비밀 키(Secret Key) 및 Brute-force 공격
서명 생성에 사용되는 비밀 키가 너무 짧거나 예측하기 쉬운 경우, 공격자는 무차별 대입(Brute-force) 공격을 통해 비밀 키를 알아낼 수 있습니다. 비밀 키가 노출되면 공격자는 유효한 서명을 가진 토큰을 직접 생성하여 시스템에 접근하거나, 기존 토큰을 변조하여 새로운 유효한 토큰을 만들 수 있습니다. 128비트 미만의 키나 흔히 사용되는 문자열(예: 'secret', 'password')은 매우 위험합니다.
Key Confusion (알고리즘 혼동) 공격
이 공격은 서버가 대칭키(HS256 등)와 비대칭키(RS256 등)를 처리하는 방식의 혼동을 이용합니다. 공격자는 alg:RS256로 설정된 토큰의 헤더를 alg:HS256으로 변경한 후, 비대칭키의 공개키를 대칭키로 사용하여 서명을 생성합니다. 만약 서버가 이를 받아들여 공개키를 대칭키로 간주하고 서명을 검증한다면, 공격자는 유효한 토큰을 만들 수 있게 됩니다. 이는 서버가 서명 알고리즘에 따라 적절한 키 유형을 사용하도록 엄격하게 검증하지 않을 때 발생합니다.
2. 데이터 유출 및 민감 정보 노출
JWT의 페이로드는 Base64Url로 인코딩될 뿐, 암호화되지 않습니다. 이는 토큰을 가로챈 누구라도 페이로드 내용을 쉽게 디코딩하여 볼 수 있다는 의미입니다. 따라서 민감한 사용자 정보(주민등록번호, 신용카드 번호, 비밀번호 등)를 페이로드에 직접 포함하는 것은 매우 위험합니다. 이러한 정보는 네트워크 스니핑이나 로그 파일 등을 통해 유출될 수 있습니다.
페이로드에는 사용자 ID나 권한과 같이 서비스 운영에 꼭 필요한 최소한의 정보만 포함해야 합니다. 민감 정보는 서버 측에 별도로 저장하고, 필요한 경우 JWT 페이로드의 ID를 통해 서버에서 조회하는 방식으로 처리해야 합니다.
3. 재전송 공격(Replay Attack) 및 만료 시간 관리
만료되지 않은 JWT가 탈취될 경우, 공격자는 해당 토큰을 사용하여 마치 원래 사용자처럼 서비스에 접근할 수 있습니다. 이를 재전송 공격이라고 합니다. JWT는 기본적으로 Stateless하기 때문에 서버에서 토큰을 '무효화'하기 어렵다는 특성을 가집니다.
- 짧은 만료 시간: 토큰의 탈취 위험을 줄이기 위해 Access Token의 만료 시간을 짧게 설정하는 것이 중요합니다. 예를 들어 5분에서 30분 정도로 설정합니다.
- Refresh Token 활용: Access Token의 만료 시간이 짧으면 사용자는 자주 재인증해야 하는 불편함이 있습니다. 이를 해결하기 위해 Refresh Token을 사용합니다. Refresh Token은 만료 시간이 길지만, Access Token 재발급 용도로만 사용되며, 한 번만 사용되도록(One-time use) 하거나 서버에 저장하여 관리하는 것이 일반적입니다. Refresh Token 자체도 탈취될 경우 위험하므로, HTTP-Only 쿠키와 같은 안전한 방식으로 저장해야 합니다.
4. CSRF 및 XSS 공격과의 연관성
JWT는 그 자체로 CSRF(Cross-Site Request Forgery) 공격을 방어하지는 않습니다. 만약 JWT를 쿠키에 저장한다면, CSRF 공격에 취약해질 수 있습니다. 공격자가 사용자 모르게 위조된 요청을 보내면, 브라우저는 해당 도메인의 쿠키(JWT 포함)를 자동으로 첨부하여 서버로 보냅니다.
XSS(Cross-Site Scripting) 공격은 공격자가 악성 스크립트를 웹 페이지에 주입하여 사용자의 세션 정보(JWT 포함)를 탈취하는 방식입니다. 만약 JWT가 JavaScript를 통해 접근 가능한 localStorage에 저장되어 있다면, XSS 공격에 매우 취약합니다. 공격자는 document.cookie나 localStorage.getItem('jwt') 등을 통해 토큰을 쉽게 빼낼 수 있습니다.
Image by Tumisu on Pixabay
안전한 JWT 구현을 위한 핵심 전략
앞서 살펴본 취약점들을 방어하고, 안전한 JWT 기반 인증 시스템을 구축하기 위한 구체적인 전략들을 제시합니다.
1. 강력한 서명 알고리즘 및 키 관리
- 강력한 서명 알고리즘 사용:
HS256,RS256과 같은 검증된 알고리즘을 사용하고,"alg":"none"과 같은 위험한 알고리즘은 절대 허용하지 않도록 서버 측에서 엄격하게 검증해야 합니다. JWT 라이브러리 설정 시 허용 가능한 알고리즘 목록을 명시적으로 지정하는 것이 좋습니다. - 길고 복잡한 비밀 키 사용: 최소 256비트(32바이트) 이상의 길이로 예측 불가능한 강력한 비밀 키를 사용해야 합니다. 이 키는 환경 변수, 키 관리 시스템(KMS) 등 안전한 곳에 저장하고, 코드베이스에 직접 포함하지 않아야 합니다. 주기적으로 키를 변경(Key Rotation)하는 정책을 고려할 수도 있습니다.
- 키 분리 및 관리: Access Token과 Refresh Token에 서로 다른 비밀 키를 사용하는 것을 고려해 볼 수 있습니다. 이는 한 키가 유출되더라도 다른 토큰의 보안에 미치는 영향을 최소화합니다.
// 예시: Node.js (jsonwebtoken 라이브러리)에서 alg:none 방지
// verify 함수에 허용할 알고리즘을 명시적으로 지정
const jwt = require('jsonwebtoken');
const secretKey = process.env.JWT_SECRET_KEY; // 환경 변수에서 로드
try {
const decoded = jwt.verify(token, secretKey, { algorithms: ['HS256', 'RS256'] });
// 토큰 유효
} catch (err) {
// 토큰 유효하지 않음 (alg:none 또는 서명 불일치 등)
console.error('Invalid JWT:', err.message);
}
2. 토큰 탈취 방지 및 저장 전략
토큰이 탈취되는 것을 막는 것은 JWT 보안의 핵심입니다. 클라이언트 측에 JWT를 저장하는 방식에 따라 보안성이 크게 달라집니다.
| 특징 | Local Storage / Session Storage | HTTP-Only Cookie |
|---|---|---|
| XSS 공격 취약성 | 매우 취약 (JavaScript 접근 가능) | 낮음 (JavaScript 접근 불가능) |
| CSRF 공격 취약성 | 낮음 (수동으로 헤더에 포함해야 함) | 높음 (브라우저가 자동 첨부) |
| 사용 편의성 | 높음 (JavaScript로 쉽게 제어) | 낮음 (서버에서 설정) |
| 권장 용도 | 비민감 데이터, 짧은 수명의 토큰 (주의 필요) | Refresh Token, 민감한 Access Token |
HTTP-Only쿠키 사용: Refresh Token과 같이 민감하고 장수명인 토큰은HTTP-Only및Secure속성이 설정된 쿠키에 저장하는 것이 가장 안전합니다.HTTP-Only는 JavaScript가 쿠키에 접근하는 것을 막아 XSS 공격으로부터 토큰을 보호합니다.Secure속성은 HTTPS를 통해서만 쿠키가 전송되도록 하여 중간자 공격을 방지합니다.- CSRF 방어 메커니즘: JWT를
HTTP-Only쿠키에 저장할 경우 CSRF에 취약해지므로, CSRF 토큰(SameSite 속성)을 함께 사용하여 방어해야 합니다.SameSite=Lax또는SameSite=Strict속성을 쿠키에 추가하면 다른 도메인에서의 요청에 쿠키가 자동으로 첨부되지 않도록 할 수 있습니다. - Access Token은 헤더에: Access Token은 일반적으로
Authorization헤더에Bearer스키마와 함께 포함하여 전송합니다. 이는 XSS에 의한 탈취 위험을 줄이기 위해localStorage에 저장하지 않는 것이 좋습니다. 단,httpOnly쿠키를 사용하여 Access Token을 저장하고, 클라이언트가 요청 시 JavaScript를 통해 쿠키를 읽어 헤더에 삽입하는 방식도 가능합니다.
3. 만료 시간 및 Refresh Token 활용
만료 시간 관리는 재전송 공격을 방어하는 데 중요합니다.
- 짧은 Access Token 만료 시간: Access Token은 짧은 만료 시간(예: 5~30분)을 가지도록 설계해야 합니다. 토큰이 탈취되더라도 공격자가 사용할 수 있는 시간을 최소화합니다.
- Refresh Token과 Access Token 분리: Access Token이 만료되면, 더 긴 만료 시간을 가진 Refresh Token을 사용하여 새로운 Access Token을 발급받습니다. Refresh Token은
HTTP-Only쿠키에 저장하고, 서버 측 데이터베이스에 저장하여 관리하는 것이 일반적입니다. - Refresh Token 무효화: Refresh Token은 사용자가 로그아웃하거나, 비밀번호를 변경하거나, 의심스러운 활동이 감지될 경우 서버에서 즉시 무효화할 수 있어야 합니다. 이는 데이터베이스에서 해당 Refresh Token을 삭제하거나 블랙리스트에 추가하는 방식으로 구현할 수 있습니다.
4. 토큰 무효화 메커니즘
JWT의 Stateless 특성 때문에 서버에서 토큰을 직접 '삭제'하기 어렵지만, 다음과 같은 방법으로 무효화 효과를 낼 수 있습니다.
- 블랙리스트(Blacklist): 만료되지 않은 Access Token을 강제로 무효화해야 할 때 (예: 로그아웃, 비밀번호 변경, 의심스러운 활동 감지), 해당 토큰을 서버의 블랙리스트(Redis 등의 빠른 저장소 활용)에 추가하고, 모든 요청 시 블랙리스트를 확인하여 유효성을 검사합니다.
- 화이트리스트(Whitelist): 발급된 토큰들을 서버에 저장하고, 요청이 올 때마다 해당 토큰이 서버에 존재하는지 확인합니다. 이는 Stateless의 장점을 일부 상쇄하지만, 더 강력한 통제력을 제공합니다. 주로 Refresh Token 관리에 사용됩니다.
- 강제 재인증: 중요한 작업(예: 결제, 개인 정보 변경)을 수행하기 전에 사용자에게 다시 비밀번호를 입력하게 하여 재인증을 요구하는 방식입니다. 이는 현재 Access Token의 유효성과 별개로 보안을 강화합니다.
Image by stevepb on Pixabay
실제 시스템에 적용하기 위한 고려사항
위에서 제시된 전략들을 실제 시스템에 적용할 때는 다음과 같은 추가적인 사항들을 고려해야 합니다.
- HTTPS 사용 강제: 모든 통신은 반드시 HTTPS를 통해 이루어져야 합니다. JWT는 전송 계층 보안(TLS/SSL)에 의존하여 토큰 가로채기(Man-in-the-Middle) 공격으로부터 보호됩니다.
Secure속성 쿠키 사용은 필수입니다. - 로깅 및 모니터링: JWT 관련 오류(예: 서명 검증 실패, 만료된 토큰 사용 시도)를 철저히 로깅하고 모니터링해야 합니다. 비정상적인 접근 시도를 빠르게 감지하고 대응할 수 있도록 시스템을 구축해야 합니다.
- JWT 라이브러리 선택 및 업데이트: 검증되고 널리 사용되는 JWT 라이브러리를 선택하고, 항상 최신 버전으로 유지하여 알려진 취약점 패치를 적용해야 합니다. 직접 JWT 구현을 시도하기보다는 전문 라이브러리를 활용하는 것이 안전합니다.
- 페이로드 최소화: JWT 페이로드에는 인증 및 권한 부여에 필요한 최소한의 정보만 포함하고, 민감 정보는 절대 포함하지 않습니다. 필요한 추가 정보는 서버에서 조회하는 방식으로 처리합니다.
- 오류 메시지 최소화: JWT 검증 실패 시 사용자에게 너무 상세한 오류 메시지를 제공하지 않도록 합니다. "토큰이 유효하지 않습니다"와 같이 일반적인 메시지를 사용하여 공격자에게 힌트를 주지 않아야 합니다.
결론: 안전한 JWT, 지속적인 관심이 필요하다
JWT(JSON Web Token)는 분명 웹 개발의 효율성과 확장성을 크게 높여주는 강력한 도구입니다. 하지만 그 편리함만큼이나 보안 취약점에 대한 깊이 있는 이해와 철저한 구현 전략이 동반되어야 합니다. "alg:none" 공격부터 시작하여 약한 비밀 키 사용, 민감 정보 노출, 재전송 공격, 그리고 XSS/CSRF와의 연관성까지, 다양한 위협에 노출될 수 있습니다. 안전한 JWT 구현은 단일한 기술적 해결책으로 끝나지 않습니다.
강력한 서명 알고리즘과 안전한 키 관리, HTTP-Only 및 Secure 쿠키를 활용한 토큰 저장 전략, 짧은 만료 시간과 Refresh Token의 적절한 활용, 그리고 토큰 무효화 메커니즘까지, 다층적인 보안 접근 방식이 필요합니다. 또한, HTTPS 강제, 로깅 및 모니터링, 검증된 라이브러리 사용 등 개발 전반에 걸친 보안 의식이 중요합니다.
JWT를 사용하는 시스템을 개발하고 있다면, 이 글에서 제시된 보안 취약점 분석과 안전한 구현 전략들을 반드시 숙지하고 적용하시길 바랍니다. 보안은 단 한 번의 설정으로 완성되는 것이 아니라, 지속적인 관심과 개선을 통해 유지되는 과정입니다. 여러분의 시스템은 이러한 보안 위협으로부터 안전한가요? 이 글이 여러분의 JWT 보안 강화에 실질적인 도움이 되었기를 바랍니다. 혹시 더 궁금한 점이나 공유하고 싶은 노하우가 있다면 댓글로 남겨주세요!
📌 함께 읽으면 좋은 글
- [보안] OWASP Top 10으로 웹 애플리케이션 보안 취약점 점검 및 방어 전략 완벽 가이드
- [튜토리얼] GitHub Actions 활용 웹 서비스 CI/CD 파이프라인 자동화: 직접 써본 구축 노하우
- [보안] DevSecOps 성공 전략: CI/CD 파이프라인 보안 자동화 완벽 가이드
이 글이 도움이 되셨다면 공감(♥)과 댓글로 응원해 주세요!
궁금한 점이나 다루었으면 하는 주제가 있다면 댓글로 남겨주세요.
'보안' 카테고리의 다른 글
| OAuth 2.0과 OpenID Connect로 안전한 인증/인가 시스템 구축 완벽 가이드 (0) | 2026.05.23 |
|---|---|
| API 보안 강화 전략: OWASP API Security Top 10 기반 실전 가이드 (0) | 2026.05.22 |
| DevSecOps 구현 전략: CI/CD 파이프라인에 보안 자동화 통합 (0) | 2026.05.21 |
| OWASP Top 10으로 웹 애플리케이션 보안 취약점 점검 및 방어 전략 완벽 가이드 (0) | 2026.05.19 |
| 시크릿 관리 솔루션 도입 전략: HashiCorp Vault vs AWS Secrets Manager 비교 분석 (0) | 2026.05.18 |