JWT 보안: 올바른 사용법과 취약점

JWT(JSON Web Token)의 구조와 검증 원리를 설명하고, alg:none 공격, 알고리즘 혼동, 약한 서명 키 등 주요 취약점과 방어 방법을 코드와 함께 다룹니다.

· 3 min read · PALDYN Team

지난 글에서 세션 기반 인증의 취약점을 다뤘다. 최근에는 세션 쿠키 대신 **JWT(JSON Web Token)**을 인증 수단으로 사용하는 경우가 많다. JWT는 강력하지만, 잘못 구현하면 심각한 보안 취약점이 생긴다.

JWT란 무엇인가

JWT는 세 부분(Header, Payload, Signature)을 점(.)으로 연결한 Base64URL 인코딩 문자열이다. Payload는 암호화가 아닌 Base64URL 인코딩이다. 누구나 디코딩해서 내용을 볼 수 있다.

JWT 구조와 검증 흐름

안전한 JWT 발급

import jwt
import secrets
from datetime import datetime, timedelta, timezone

JWT_SECRET = os.environ["JWT_SECRET"]  # 256비트 이상
JWT_ALGORITHM = "HS256"

def create_access_token(user_id: str, roles: list[str]) -> str:
    now = datetime.now(timezone.utc)
    payload = {
        "sub": user_id,
        "roles": roles,
        "iat": now,
        "exp": now + timedelta(minutes=15),
        "type": "access",
    }
    return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)

JWT 주요 취약점과 방어

주요 취약점과 방어

① alg:none 공격

# 취약: alg를 토큰에서 그대로 사용
# token_data = jwt.decode(token, key)  # 절대 금지

# 안전: 알고리즘 명시적 화이트리스트
token_data = jwt.decode(
    token,
    key=JWT_SECRET,
    algorithms=["HS256"],  # "none" 포함 불가
)

② 약한 서명 키

# 나쁜 예
JWT_SECRET = "secret123"  # 절대 금지

# 좋은 예: 256비트 이상 무작위
import secrets
JWT_SECRET = secrets.token_hex(32)  # 생성 후 환경 변수에 저장

토큰 검증 함수

from fastapi import HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

security = HTTPBearer()

def verify_token(
    credentials: HTTPAuthorizationCredentials = Security(security)
) -> dict:
    try:
        payload = jwt.decode(
            credentials.credentials,
            JWT_SECRET,
            algorithms=["HS256"],
            options={"require": ["exp", "sub", "iat"]},
        )
        if payload.get("type") != "access":
            raise HTTPException(status_code=401, detail="Invalid token type")
        return payload
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="Token expired")
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Invalid token")

로그아웃: 토큰 취소

import uuid

def create_token_with_jti(user_id: str) -> str:
    payload = {
        "sub": user_id,
        "jti": str(uuid.uuid4()),
        "exp": datetime.utcnow() + timedelta(minutes=15),
    }
    return jwt.encode(payload, JWT_SECRET, algorithm="HS256")

async def logout(token: str):
    payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
    jti = payload["jti"]
    ttl = int(payload["exp"] - time.time()) + 10
    await redis.setex(f"jwt_blacklist:{jti}", ttl, "1")

JWT vs 세션 쿠키

기준JWT세션 쿠키
서버 상태StatelessStateful
수평 확장용이Redis 등 필요
즉각 취소어려움즉시 가능
보안 구현 복잡도높음낮음

일반적인 웹 앱은 세션 쿠키가 더 단순하고 안전하다. API 서버나 마이크로서비스에서 여러 서버 간 인증이 필요할 때 JWT가 유용하다.


지난 글: 세션 하이재킹(Session Hijacking) 완전 정복


읽어주셔서 감사합니다. 😊