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) 완전 정복


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