보안

JWT 보안 취약점 심층 분석 및 안전한 토큰 구현 가이드

강코의 코딩 일기 2026. 6. 20. 07:21
반응형

실제 프로젝트에서 겪은 JWT 보안 취약점들을 분석하고, 안전한 JWT 구현을 위한 실용적인 가이드를 제공합니다. JWT를 안전하게 사용하는 방법은 무엇일까요?

안녕하세요, 여러분. 백엔드 개발자로 일하면서 수많은 인증/인가 시스템을 구축하고 개선해 왔습니다. 그중에서도 JWT(JSON Web Token)는 stateless 한 특성 덕분에 마이크로서비스 아키텍처나 모바일 애플리케이션 환경에서 매우 유용하게 사용되는 기술이죠. 저 역시 RESTful API 서버를 개발할 때마다 JWT를 적극적으로 활용해 왔습니다.

하지만 JWT가 만능 열쇠처럼 보일지라도, 실제로 적용해 보니 그 이면에 숨겨진 다양한 보안 취약점들이 존재한다는 것을 알게 되었습니다. "JWT를 사용하면 안전하다"는 막연한 믿음으로 접근했다가, 나중에 심각한 보안 문제를 겪을 뻔한 아찔한 경험도 있었죠. 이 글에서는 제가 직접 겪고 분석했던 JWT 보안 취약점들과 이를 방지하기 위한 안전한 사용 가이드를 실무 경험을 바탕으로 상세하게 공유하고자 합니다. 과연 여러분의 서비스는 JWT를 안전하게 사용하고 있을까요?

📑 목차

JWT(JSON Web Token) 보안 취약점 분석 및 안전한 사용 가이드 - computer, security, company, secure id, token, security, token, token, token, token, token

Image by Lalmch on Pixabay

JWT의 핵심 구조와 작동 방식 이해: 오해는 금물!

JWT의 보안 취약점을 제대로 이해하려면, 먼저 그 구조와 작동 방식을 정확히 아는 것이 중요합니다. 많은 개발자들이 JWT를 단순히 "암호화된 토큰"으로 오해하는 경우가 있는데, 이는 매우 위험한 생각입니다. JWT는 크게 세 부분으로 구성됩니다.

  1. Header (헤더): 토큰의 타입(`typ`)과 서명에 사용된 알고리즘(`alg`) 정보가 담깁니다. 예를 들어, {"alg": "HS256", "typ": "JWT"}와 같습니다.
  2. Payload (페이로드): 클레임(Claim)이라고 불리는 실제 정보들이 담기는 부분입니다. 사용자 ID, 권한, 토큰 발행 시간(`iat`), 만료 시간(`exp`) 등 다양한 정보를 포함할 수 있습니다. 예를 들어, {"userId": "123", "role": "admin", "exp": 1678886400}와 같습니다.
  3. Signature (서명): 헤더와 페이로드를 Base64 URL-safe 인코딩한 값을 서버의 비밀 키(Secret Key)로 서명한 부분입니다. 이 서명이 JWT의 무결성(Integrity)을 보장하는 핵심 요소입니다.

이 세 부분이 .으로 연결되어 하나의 JWT 문자열이 됩니다. 여기서 중요한 점은 HeaderPayload암호화(Encryption)된 것이 아니라 단순히 Base64 인코딩된 것이라는 사실입니다. 즉, 누구나 JWT를 디코딩하여 헤더와 페이로드의 내용을 쉽게 확인할 수 있습니다. JWT가 보장하는 것은 데이터의 무결성(서명 이후 내용이 위변조되지 않았음)이지, 데이터의 기밀성(내용이 숨겨져 있음)이 아닙니다. 이 점을 간과하면 치명적인 보안 취약점으로 이어질 수 있습니다.

실제로 마주친 JWT 보안 취약점들 분석

제가 개발 현장에서 JWT를 사용하면서 가장 흔히 발견했거나, 잠재적으로 위험하다고 판단했던 취약점들을 공유합니다. 이 사례들은 단순히 이론적인 문제가 아니라, 실제로 서비스에 위협이 될 수 있는 시나리오들입니다.

서명(Signature) 검증 우회 취약점 (None 알고리즘, 키 변경)

가장 악명 높은 JWT 보안 취약점 중 하나입니다. 공격자가 JWT의 헤더에 있는 "alg" 필드를 "none"으로 변경하고, 서명 부분을 제거한 후 서버에 전송하면, 일부 라이브러리나 잘못 구현된 서버는 이 토큰을 유효한 것으로 처리할 수 있습니다. "none" 알고리즘은 말 그대로 서명이 없다는 의미이므로, 서버는 서명 검증을 시도하지 않고 페이로드만 파싱하여 사용하게 됩니다.

실제로 제가 경험했던 프로젝트에서는 JWT 라이브러리 사용 시, 기본 설정이 "none" 알고리즘을 허용하도록 되어 있었습니다. 개발 초기 단계에서는 테스트 편의성을 위해 이런 설정을 간과하기 쉬운데, 프로덕션 환경에서는 반드시 특정 알고리즘(예: HS256, RS256)만 허용하도록 명시적으로 설정해야 합니다. 만약 공격자가 userIdadmin으로 변경한 페이로드를 "none" 알고리즘으로 서명 없이 전송한다면, 관리자 권한을 탈취할 수 있는 심각한 상황이 발생합니다.

또 다른 형태는 키 변경(Key Confusion) 공격입니다. 예를 들어, 서버가 비대칭 키(RSA) 방식으로 서명된 JWT를 기대하지만, 공격자가 대칭 키(HS256) 방식으로 위조된 JWT를 생성하여 서버의 공개 키(Public Key)비밀 키(Secret Key)처럼 사용하여 서명하는 공격입니다. 서버는 공개 키로 HS256 서명을 검증하려 하지만, 공개 키는 사실 비밀 키가 아니므로 잘못된 방식으로 검증하게 되어 공격이 성공할 수 있습니다. 이는 라이브러리의 구현 방식이나 서버 측의 검증 로직이 충분히 견고하지 않을 때 발생할 수 있습니다.

무작위 대입 공격(Brute-force) 및 Secret Key 노출 위험

JWT의 서명은 서버의 비밀 키(Secret Key)를 통해 이루어집니다. 이 비밀 키가 짧거나 예측 가능성이 높다면, 공격자는 무작위 대입 공격(Brute-force attack)을 통해 비밀 키를 알아낼 수 있습니다. 비밀 키가 노출되면 공격자는 얼마든지 유효한 JWT를 위조하여 시스템에 접근할 수 있게 됩니다. 이는 시스템 전체의 보안을 위협하는 치명적인 문제입니다.

제가 참여했던 한 프로젝트에서는 개발 환경에서 사용하던 간단한 문자열을 프로덕션 Secret Key로 그대로 사용하는 실수를 저지를 뻔했습니다. 다행히 코드 리뷰 단계에서 발견하여 수정했지만, 이런 기본적인 실수가 실제 서비스에서 발생하면 걷잡을 수 없는 피해로 이어질 수 있습니다. 비밀 키는 최소 32자 이상의 무작위 문자열로 구성하고, 환경 변수KMS(Key Management Service)와 같은 안전한 방법으로 관리해야 합니다. 절대로 소스코드에 하드코딩해서는 안 됩니다.

# 나쁜 예: 소스코드에 하드코딩
SECRET_KEY = "my_super_secret_key"

# 좋은 예: 환경 변수 사용
SECRET_KEY = os.environ.get("JWT_SECRET_KEY")

토큰 탈취(Token Theft) 및 재사용 공격

JWT는 클라이언트 측에 저장되기 때문에, XSS(Cross-Site Scripting) 공격이나 CSRF(Cross-Site Request Forgery) 공격에 취약할 수 있습니다. XSS 공격을 통해 악성 스크립트가 클라이언트 브라우저에서 실행되면, 공격자는 localStoragesessionStorage에 저장된 JWT를 쉽게 탈취할 수 있습니다. 탈취된 JWT는 공격자가 마치 정당한 사용자처럼 서버에 요청을 보낼 수 있게 만듭니다.

실제로 프론트엔드 개발 시 localStorage액세스 토큰을 저장하는 경우가 많습니다. 이는 편리하지만 XSS에 노출될 경우 심각한 결과를 초래할 수 있습니다. 저도 XSS 취약점이 있는 게시판에 악성 스크립트를 삽입하여 다른 사용자의 JWT를 탈취하는 모의 해킹 시나리오를 직접 구현해보고 그 위험성을 절감했습니다. 탈취된 토큰은 만료되기 전까지 계속해서 재사용될 수 있으며, 이는 사용자의 세션을 완전히 탈취하는 것과 마찬가지입니다.

페이로드 위변조 및 민감 정보 노출

앞서 언급했듯이, JWT의 페이로드는 Base64 인코딩되어 누구나 디코딩하여 내용을 볼 수 있습니다. 서명은 페이로드의 무결성을 보장하지만, 기밀성을 보장하지 않습니다. 따라서 페이로드에 사용자 비밀번호, 주민등록번호, 신용카드 정보 등 민감한 개인 정보를 직접 저장하는 것은 매우 위험합니다. 공격자가 토큰을 탈취하지 않고 단순히 디코딩하는 것만으로도 민감 정보가 노출될 수 있습니다.

제가 개발했던 초기 버전의 시스템에서는 페이로드에 사용자의 상세 권한 목록을 모두 넣었던 적이 있습니다. 나중에 보안 검토를 하면서 이 부분이 문제가 될 수 있음을 깨달았습니다. 만약 권한 정보에 특정 관리자용 기능 접근 권한이 상세하게 명시되어 있다면, 공격자가 이를 보고 특정 공격 시나리오를 구상할 수 있는 단서가 될 수도 있습니다. JWT 페이로드에는 최소한의 정보, 즉 사용자 ID나 역할 등 꼭 필요한 정보만 포함하고, 민감 정보는 서버 측에서 별도로 관리하는 것이 바람직합니다.

토큰 만료 관리의 복잡성과 리프레시 토큰의 양날의 검

JWT는 기본적으로 stateless하기 때문에, 서버는 토큰의 상태를 추적하지 않습니다. 이는 확장성에는 유리하지만, 한 번 발급된 토큰은 만료되기 전까지 유효하다는 의미입니다. 사용자가 로그아웃하거나 비밀번호를 변경해도, 만료되지 않은 액세스 토큰은 여전히 유효할 수 있습니다. 이 때문에 토큰 만료 시간을 너무 길게 설정하면, 토큰 탈취 시 공격자가 유효한 토큰을 사용할 수 있는 시간이 길어져 위험합니다.

이 문제를 해결하기 위해 도입되는 것이 리프레시 토큰(Refresh Token)입니다. 짧은 만료 시간을 가진 액세스 토큰과 긴 만료 시간을 가진 리프레시 토큰을 함께 사용하는 전략이죠. 액세스 토큰이 만료되면 리프레시 토큰으로 새로운 액세스 토큰을 발급받습니다. 하지만 리프레시 토큰 자체가 탈취되면, 공격자는 계속해서 새로운 액세스 토큰을 발급받아 시스템에 접근할 수 있게 됩니다. 실제로 한 프로젝트에서 리프레시 토큰localStorage에 저장했다가, XSS 취약점으로 인해 리프레시 토큰까지 탈취당하는 상황을 가정하고 보안 대책을 강구해야 했습니다. 리프레시 토큰액세스 토큰보다 훨씬 더 강력한 보안 조치로 보호해야 합니다.

JWT(JSON Web Token) 보안 취약점 분석 및 안전한 사용 가이드 - padlock, lock, chain, key, security, protection, safety, access, locked, link, crime, steel, privacy, secure, criminal, shackle, danger, thief, theft, vulnerable, restrain, break-in, protect, strong, padlock, padlock, lock, lock, lock, lock, lock, chain, crime, privacy, privacy, thief, thief, theft, strong

Image by stevepb on Pixabay

안전한 JWT 사용을 위한 실용적인 가이드라인

앞서 분석한 취약점들을 바탕으로, 제가 실제로 시스템에 적용하고 있는 안전한 JWT 사용 가이드라인을 공유합니다. 이 가이드라인을 따르면 JWT의 장점을 활용하면서도 보안 위험을 크게 줄일 수 있습니다.

강력한 서명 알고리즘 및 키 관리

  • 알고리즘 선택: "none" 알고리즘은 절대 사용하지 마세요. HS256, HS512(대칭 키) 또는 RS256, ES256(비대칭 키)과 같이 강력한 서명 알고리즘을 사용해야 합니다. 비대칭 키 방식은 공개 키를 통해 서명을 검증하므로, 클라이언트에게 공개 키를 노출해도 안전하다는 장점이 있습니다.
  • 비밀 키(Secret Key) 관리: 비밀 키는 외부로 노출되지 않도록 철저히 관리해야 합니다.
    • 환경 변수(Environment Variables): 서버 실행 시 환경 변수로 주입하는 것이 가장 기본적인 방법입니다.
    • KMS(Key Management Service): AWS KMS, Azure Key Vault, Google Cloud KMS 등 클라우드에서 제공하는 키 관리 서비스를 활용하면 비밀 키를 더욱 안전하게 저장하고 관리할 수 있습니다.
    • 길이 및 복잡성: 비밀 키는 최소 32자 이상의 예측 불가능한 복잡한 문자열로 구성해야 합니다.
    • 주기적인 교체: 비밀 키를 주기적으로 교체하는 정책을 수립하는 것이 좋습니다.

토큰 만료 시간 최소화 및 리프레시 토큰 전략

  • 액세스 토큰 만료 시간: 액세스 토큰은 만료 시간을 매우 짧게 가져가는 것이 중요합니다. 보통 5분에서 30분 정도로 설정하며, 이는 탈취되더라도 공격자가 사용할 수 있는 시간을 최소화하기 위함입니다.
  • 리프레시 토큰 활용:
    • 리프레시 토큰액세스 토큰보다 긴 만료 시간을 가지며, 새로운 액세스 토큰을 발급하는 용도로만 사용합니다.
    • HTTP Only Cookie: 리프레시 토큰HTTP Only Cookie에 저장하여 XSS 공격으로부터 보호해야 합니다. JavaScript에서 접근할 수 없으므로 탈취 위험이 줄어듭니다.
    • Secure Flag: HTTPS 통신에서만 쿠키가 전송되도록 Secure 플래그를 설정해야 합니다.
    • CSRF 토큰: 리프레시 토큰을 사용하는 API 호출에는 CSRF 토큰을 함께 사용하여 CSRF 공격을 방지해야 합니다.
    • 일회성 리프레시 토큰 (Refresh Token Rotation): 리프레시 토큰을 한 번 사용하면 새로운 리프레시 토큰을 발급하고 기존 토큰은 무효화하는 전략입니다. 만약 리프레시 토큰이 탈취되어 사용되더라도, 공격자는 한 번만 사용할 수 있고 사용 즉시 기존 토큰은 무효화되므로 공격을 감지하고 대응하기 용이합니다.

민감 정보는 페이로드에 포함하지 않기

JWT 페이로드에는 사용자 ID, 역할 등 인증/인가에 필수적인 최소한의 정보만 포함해야 합니다. 개인 식별 정보, 금융 정보 등 민감한 데이터는 절대 페이로드에 저장해서는 안 됩니다. 필요한 경우, 페이로드의 사용자 ID를 이용해 백엔드에서 데이터베이스를 조회하여 추가 정보를 가져오는 방식으로 처리해야 합니다.

토큰 무효화(Invalidation) 및 블랙리스트 관리

JWT는 기본적으로 stateless하지만, 특정 상황(로그아웃, 비밀번호 변경, 강제 세션 종료 등)에서는 발급된 토큰을 즉시 무효화해야 할 필요가 있습니다. 이를 위해 토큰 블랙리스트 또는 세션 관리 메커니즘을 구현할 수 있습니다.

  • 블랙리스트: 무효화해야 할 액세스 토큰의 ID(JTI)를 Redis와 같은 인메모리 데이터베이스에 저장하고, 모든 요청에 대해 토큰이 블랙리스트에 있는지 확인합니다. 토큰의 남은 만료 시간 동안만 저장하면 됩니다.
  • 세션 관리: 리프레시 토큰의 경우, 서버 측에서 사용자별 리프레시 토큰을 관리하는 테이블을 두어 토큰을 즉시 무효화할 수 있도록 합니다. 예를 들어, 사용자가 로그아웃하면 해당 사용자의 모든 리프레시 토큰을 데이터베이스에서 삭제하는 방식입니다.

전송 보안 강화 (HTTPS 필수)

JWT는 네트워크를 통해 전송되므로, MITM(Man-in-the-Middle) 공격으로부터 보호하기 위해 모든 통신은 반드시 HTTPS를 통해 암호화되어야 합니다. HTTP를 사용하면 JWT가 평문으로 노출되어 쉽게 탈취될 수 있습니다. 이는 JWT뿐만 아니라 모든 웹 서비스 보안의 기본입니다.

클라이언트 측 토큰 저장 방식 주의

액세스 토큰을 클라이언트 측에 저장할 때, XSS 공격에 취약한 localStorage보다는 상대적으로 안전한 방법을 고려해야 합니다.

  • sessionStorage: 브라우저 세션이 종료되면 자동으로 삭제되므로 localStorage보다 안전합니다.
  • HTTP Only Cookie: 액세스 토큰 역시 HTTP Only Cookie에 저장하여 JavaScript 접근을 막는 것을 고려할 수 있습니다. 다만, CSRF 공격에 취약해질 수 있으므로 CSRF 토큰 등 추가적인 방어책이 필요합니다.
  • 보안 라이브러리 활용: React, Vue 등 프론트엔드 프레임워크에서 제공하는 보안 미들웨어나 라이브러리를 활용하여 토큰 저장 및 관리를 더욱 안전하게 할 수 있습니다.
JWT(JSON Web Token) 보안 취약점 분석 및 안전한 사용 가이드 - man, face, facial recognition, biometric, identify, security, people, authentication, identification, database, scanning, facial recognition, facial recognition, facial recognition, facial recognition, facial recognition, biometric

Image by Tumisu on Pixabay

JWT vs 세션 기반 인증: 어떤 것을 선택해야 할까?

JWT의 보안 취약점을 이해하고 나면, "그럼 세션 기반 인증이 더 안전한가?"라는 의문이 들 수 있습니다. 두 방식 모두 장단점이 명확하며, 프로젝트의 요구사항과 아키텍처에 따라 적절한 선택이 필요합니다.

특징 JWT 기반 인증 세션 기반 인증
상태 관리 Stateless (서버에 상태 저장 안 함) Stateful (서버에 세션 정보 저장)
확장성 매우 우수 (서버 간 세션 공유 불필요) 세션 공유를 위한 추가적인 인프라 필요 (Redis 등)
크로스 도메인 유리 (토큰만 전송하면 됨) CORS 설정 및 쿠키 처리 복잡
모바일 앱 적합 (쿠키 의존성 낮음) 쿠키 기반이므로 구현 복잡
보안 취약점 토큰 탈취, 서명 우회, 민감 정보 노출 등 세션 고정, 세션 탈취(쿠키), CSRF 등
토큰 무효화 구현 복잡 (블랙리스트 필요) 서버에서 세션 삭제로 쉽게 가능

제가 실무에서 겪어본 결과, JWT는 마이크로서비스 아키텍처나 모바일/SPA(Single Page Application)와 같이 Stateless한 환경이 필수적이거나, 여러 도메인 간의 인증을 처리해야 할 때 매우 강력한 이점을 가집니다. 특히 서버의 부하를 줄이고 싶을 때 효과적입니다. 반면, 전통적인 웹 애플리케이션이나 토큰 무효화가 즉각적으로 이뤄져야 하는 강력한 보안 요구사항이 있는 경우에는 세션 기반 인증이 더 적합할 수 있습니다.

궁극적으로는 두 방식 모두 나름의 보안 취약점을 가지고 있으므로, 어떤 방식을 선택하든 해당 방식에 대한 깊은 이해와 철저한 보안 고려가 동반되어야 합니다. 저는 주로 JWT를 사용하되, 위에 설명한 리프레시 토큰블랙리스트 전략을 결합하여 세션 기반 인증의 장점(토큰 무효화)을 일부 가져오는 방식으로 구현해 왔습니다.

마무리: JWT, 알면 약 모르면 독

JWT(JSON Web Token)는 분명 강력하고 유연한 인증 메커니즘입니다. 하지만 "나는 JWT를 쓰고 있으니 안전하다"는 안일한 생각은 금물입니다. 제가 직접 경험하고 분석했듯이, JWT는 잘못 사용했을 때 심각한 보안 취약점으로 이어질 수 있는 요소들을 내포하고 있습니다. Header, Payload, Signature 각 부분이 어떻게 작동하는지 정확히 이해하고, 서명 알고리즘 선택, 비밀 키 관리, 토큰 만료 전략, 클라이언트 측 저장 방식 등 모든 단계에서 보안을 최우선으로 고려해야 합니다.

결국 JWT는 개발자의 손에 달려 있습니다. 그 장점을 최대한 활용하면서도 잠재적인 위험을 최소화하기 위해서는 끊임없이 학습하고, 최신 보안 동향을 파악하며, 실제 프로젝트에 적용하는 과정에서 꼼꼼하게 검토해야 합니다. 이 글이 여러분의 서비스가 JWT를 더욱 안전하게 사용할 수 있는 데 도움이 되기를 바랍니다. 여러분은 JWT를 어떻게 안전하게 사용하고 계신가요? 댓글로 여러분의 경험과 노하우를 공유해 주세요!

📌 함께 읽으면 좋은 글

  • [클라우드 인프라] Terraform으로 클라우드 인프라 자동화: IaC 설계 원칙과 실전 가이드
  • [AI 머신러닝] LLM 기반 자율 에이전트 개발: 핵심 설계 원칙과 실전 가이드
  • [이슈 분석] 원격 및 하이브리드 근무, 개발자 협업과 생산성에 미치는 영향 분석

이 글이 도움이 되셨다면 공감(♥)댓글로 응원해 주세요!
궁금한 점이나 다루었으면 하는 주제가 있다면 댓글로 남겨주세요.

반응형