지식
Security
JWT 보안: 올바른 사용법과 취약점
JWT(JSON Web Token)의 구조와 검증 원리를 설명하고, alg:none 공격, 알고리즘 혼동, 약한 서명 키 등 주요 취약점과 방어 방법을 코드와 함께 다룹니다.
지난 글에서 세션 기반 인증의 취약점을 다뤘다. 최근에는 세션 쿠키 대신 JWT(JSON Web Token)을 인증 수단으로 사용하는 경우가 많다. JWT는 강력하지만, 잘못 구현하면 심각한 보안 취약점이 생긴다.
JWT란 무엇인가
JWT는 세 부분(Header, Payload, Signature)을 점(.)으로 연결한 Base64URL 인코딩 문자열이다. Payload는 암호화가 아닌 Base64URL 인코딩이다. 누구나 디코딩해서 내용을 볼 수 있다.
안전한 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)
주요 취약점과 방어
① 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 | 세션 쿠키 |
|---|---|---|
| 서버 상태 | Stateless | Stateful |
| 수평 확장 | 용이 | Redis 등 필요 |
| 즉각 취소 | 어려움 | 즉시 가능 |
| 보안 구현 복잡도 | 높음 | 낮음 |
일반적인 웹 앱은 세션 쿠키가 더 단순하고 안전하다. API 서버나 마이크로서비스에서 여러 서버 간 인증이 필요할 때 JWT가 유용하다.
지난 글: 세션 하이재킹(Session Hijacking) 완전 정복
읽어주셔서 감사합니다. 😊