웹보안: JWT(JSON Web Token) 보안 취약점 및 대응책 🔐
안녕, 친구들! 오늘은 웹 개발자들 사이에서 핫한 주제인 JWT(JSON Web Token)에 대해 재미있게 얘기해볼 거야. 특히 JWT의 보안 취약점과 그에 대한 대응책에 대해 깊이 있게 파헤쳐볼 거니까 집중해! 🧐
먼저, JWT가 뭔지 간단히 설명하자면, 웹에서 정보를 안전하게 주고받을 때 사용하는 일종의 디지털 신분증이라고 생각하면 돼. 근데 이 신분증, 완벽한 줄 알았더니 몇 가지 허점이 있더라고. 그래서 오늘은 이 허점들을 낱낱이 파헤치고, 어떻게 하면 더 안전하게 사용할 수 있을지 알아볼 거야. 😎
자, 그럼 시작해볼까? 🚀
1. JWT란 뭐야? 🤔
JWT, 즉 JSON Web Token은 웹에서 정보를 안전하게 전송하기 위한 개방형 표준(RFC 7519)이야. 쉽게 말해, 디지털 세계의 신분증이라고 생각하면 돼. 이 신분증은 세 부분으로 나뉘어 있어:
- 헤더 (Header): 토큰의 타입과 사용된 해시 알고리즘 정보
- 페이로드 (Payload): 전달하려는 정보 (클레임이라고도 해)
- 서명 (Signature): 토큰이 변조되지 않았음을 확인하는 서명
이 세 부분은 각각 Base64Url로 인코딩되고, 점(.)으로 구분돼서 하나의 긴 문자열로 만들어져. 그래서 JWT를 보면 이런 식으로 생겼어:
xxxxx.yyyyy.zzzzz
여기서 xxxxx는 헤더, yyyyy는 페이로드, zzzzz는 서명을 나타내. 이렇게 생긴 토큰을 클라이언트가 서버에 보내면, 서버는 이 토큰을 검증해서 사용자를 인증하는 거지. 😊
JWT의 장점은 뭐냐고? 🌟
- 상태 비저장(Stateless): 서버가 사용자의 상태를 저장할 필요가 없어. 토큰 자체에 모든 필요한 정보가 들어있거든.
- 확장성: 분산 시스템에서도 잘 작동해. 여러 서버에서도 같은 토큰으로 인증할 수 있으니까.
- 범용성: 다양한 프로그래밍 언어에서 지원돼. 자바, 파이썬, 자바스크립트 등 어디서든 쓸 수 있어.
- 효율성: 토큰 크기가 작아서 네트워크 오버헤드가 적어.
근데 말이야, 이렇게 좋은 JWT도 완벽하진 않아. 보안 취약점이 있다고? 맞아, 그래서 우리가 오늘 이 주제로 이야기하는 거야. 🕵️♀️
JWT를 사용하는 대표적인 예로는 싱글 사인온(SSO)이 있어. 한 번의 로그인으로 여러 서비스를 이용할 수 있게 해주는 거지. 예를 들어, 구글 계정으로 로그인하면 Gmail, YouTube, Google Drive 등을 한 번에 이용할 수 있잖아? 그게 바로 JWT를 이용한 SSO의 예야.
재능넷 같은 재능 공유 플랫폼에서도 JWT를 활용할 수 있어. 사용자가 로그인하면 JWT를 발급받고, 이후 재능 거래나 메시지 교환 등의 기능을 이용할 때마다 이 토큰을 사용해서 인증을 처리할 수 있지. 이렇게 하면 서버 부하도 줄이고, 사용자 경험도 개선할 수 있어. 👍
위 그림을 보면 JWT의 구조를 한눈에 이해할 수 있지? 헤더, 페이로드, 서명이 어떻게 하나의 토큰으로 합쳐지는지 보여주고 있어. 이제 JWT가 뭔지 대충 감이 왔을 거야. 그럼 이제 본격적으로 JWT의 보안 취약점에 대해 알아볼까? 🕵️♂️
2. JWT의 주요 보안 취약점 😱
자, 이제 JWT의 어두운 면을 파헤쳐볼 시간이야. JWT가 완벽해 보이지만, 사실 몇 가지 중요한 보안 취약점이 있어. 이걸 모르고 사용하다간 큰 문제가 생길 수 있으니 잘 들어봐! 🚨
2.1. 알고리즘 없음 공격 (None Algorithm Attack) 🚫
첫 번째로 알아볼 취약점은 '알고리즘 없음 공격'이야. 이게 뭐냐면, JWT의 헤더 부분에서 알고리즘을 'none'으로 설정해서 서명 검증을 우회하는 공격이야. 😮
JWT 헤더는 보통 이렇게 생겼어:
{
"alg": "HS256",
"typ": "JWT"
}
여기서 "alg"는 서명에 사용된 알고리즘을 나타내. 근데 공격자가 이걸 이렇게 바꾸면 어떻게 될까?
{
"alg": "none",
"typ": "JWT"
}
이렇게 하면 서버가 서명 검증을 건너뛰고 토큰을 유효한 것으로 간주할 수 있어. 그러면 공격자는 마음대로 페이로드를 수정해서 권한을 탈취할 수 있게 되는 거지. 😱
이런 공격을 막으려면 어떻게 해야 할까? 🤔
- 항상 알고리즘을 검증하기: 서버에서 허용하는 알고리즘 목록을 미리 정해두고, 토큰의 알고리즘이 이 목록에 있는지 확인해.
- 'none' 알고리즘 거부하기: 'none' 알고리즘을 아예 허용하지 않도록 설정해.
- 라이브러리 최신 버전 사용하기: 최신 JWT 라이브러리들은 이런 공격을 막는 기능이 내장되어 있어.
예를 들어, Node.js에서 jsonwebtoken 라이브러리를 사용한다면 이렇게 설정할 수 있어:
const jwt = require('jsonwebtoken');
// 토큰 검증 시
jwt.verify(token, secretKey, { algorithms: ['HS256', 'RS256'] }, (err, decoded) => {
if (err) {
// 에러 처리
} else {
// 토큰 유효
}
});
이렇게 하면 'HS256'과 'RS256' 알고리즘만 허용하고, 'none'을 포함한 다른 알고리즘은 모두 거부하게 돼. 👍
2.2. 서명 검증 생략 공격 🙈
두 번째로 알아볼 취약점은 '서명 검증 생략 공격'이야. 이건 서버가 JWT의 서명을 제대로 검증하지 않을 때 발생해.
JWT는 세 부분으로 나뉘어 있다고 했지? 헤더, 페이로드, 서명. 이 중에서 서명 부분이 가장 중요해. 왜냐하면 서명이 토큰의 무결성을 보장하거든. 근데 만약 서버가 이 서명을 제대로 확인하지 않으면 어떻게 될까? 🤔
공격자가 페이로드를 마음대로 수정해도 서버는 이를 탐지하지 못하게 돼. 예를 들어, 일반 사용자의 토큰을 가져와서 페이로드의 권한 부분만 수정하면 관리자 권한을 얻을 수 있는 거지. 😨
이런 공격을 막으려면:
- 항상 서명을 검증하기: 토큰을 받을 때마다 반드시 서명을 검증해야 해.
- 안전한 비밀키 사용하기: 서명 생성과 검증에 사용되는 비밀키를 안전하게 관리해야 해.
- 신뢰할 수 있는 라이브러리 사용하기: 검증된 JWT 라이브러리를 사용해서 실수를 줄여.
예를 들어, Python에서 PyJWT 라이브러리를 사용한다면 이렇게 할 수 있어:
import jwt
try:
# 토큰 검증
payload = jwt.decode(token, secret_key, algorithms=['HS256'])
# 토큰이 유효하면 여기서 페이로드를 사용할 수 있어
except jwt.InvalidSignatureError:
# 서명이 유효하지 않을 때의 처리
print("Invalid signature!")
except jwt.DecodeError:
# 토큰 디코딩 실패 시 처리
print("Token cannot be decoded!")
이렇게 하면 서명 검증을 제대로 할 수 있고, 만약 서명이 유효하지 않으면 예외가 발생해서 적절히 처리할 수 있어. 👌
2.3. 무차별 대입 공격 (Brute Force Attack) 🔨
세 번째로 알아볼 취약점은 '무차별 대입 공격'이야. 이건 공격자가 다양한 키를 시도해서 JWT의 서명을 위조하려는 공격이야.
JWT는 비밀키를 사용해서 서명을 만들어. 그런데 이 비밀키가 너무 짧거나 예측 가능하면 공격자가 이를 추측할 수 있어. 특히 HS256 같은 대칭키 알고리즘을 사용할 때 이런 위험이 더 커져. 🔑
공격자는 다음과 같은 방법으로 무차별 대입 공격을 시도할 수 있어:
- JWT의 헤더와 페이로드를 가져와.
- 다양한 키를 사용해서 서명을 생성해봐.
- 생성된 서명이 원래 JWT의 서명과 일치하는지 확인해.
- 일치하면 비밀키를 찾은 거야!
이런 공격을 막으려면 어떻게 해야 할까? 🤔
- 강력한 비밀키 사용하기: 길고 복잡한 비밀키를 사용해. 최소 256비트 이상의 랜덤한 키를 추천해.
- 비대칭 알고리즘 사용하기: RS256 같은 비대칭 알고리즘을 사용하면 공개키로는 서명을 생성할 수 없어 더 안전해져.
- 키 순환 (Key Rotation) 적용하기: 주기적으로 키를 변경해서 공격자가 키를 알아내더라도 그 영향을 최소화할 수 있어.
- 레이트 리미팅 (Rate Limiting) 적용하기: 짧은 시간 동안 너무 많은 요청이 들어오면 차단해서 무차별 대입 공격을 어렵게 만들어.
예를 들어, 비밀키를 생성할 때는 이렇게 할 수 있어:
# Python에서 안전한 랜덤 비밀키 생성하기
import secrets
secret_key = secrets.token_hex(32) # 256비트 (32바이트) 랜덤 키 생성
print(secret_key)
이렇게 생성된 키는 충분히 길고 예측 불가능해서 무차별 대입 공격을 어렵게 만들어. 😎
2.4. JWT 재사용 공격 🔄
네 번째로 알아볼 취약점은 'JWT 재사용 공격'이야. 이건 만료된 토큰이나 로그아웃한 사용자의 토큰을 다시 사용하는 공격이야.
JWT는 기본적으로 상태를 저장하지 않아(stateless). 그래서 서버는 토큰이 유효한지만 확인하고, 그 토큰이 실제로 사용 중인지는 모르는 경우가 많아. 이 특성 때문에 다음과 같은 문제가 생길 수 있어:
- 사용자가 로그아웃해도 토큰은 여전히 유효할 수 있어.
- 토큰이 탈취되면 만료될 때까지 계속 사용될 수 있어.
- 만료된 토큰을 시스템 시간을 조작해서 다시 사용할 수 있어.
이런 공격을 막으려면 어떻게 해야 할까? 🤔
- 짧은 만료 시간 설정하기: 토큰의 유효 기간을 짧게 설정해. 대신 리프레시 토큰을 사용해서 사용자 경험을 유지해.
- 토큰 블랙리스트 관리하기: 로그아웃한 토큰이나 탈취된 토큰을 블랙리스트에 추가해서 재사용을 막아.
- 토큰 회전 (Token Rotation) 구현하기: 주기적으로 새로운 토큰을 발급해서 오래된 토큰의 사용을 제한해.
- jti (JWT ID) 클레임 사용하기: 각 토큰에 고유한 ID를 부여해서 토큰의 사용 여부를 추적해.
예를 들어, Node.js에서 express와 jsonwebtoken을 사용한다면 이렇게 구현할 수 있어:
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
// 블랙리스트 (실제로는 데이터베이스를 사용하는 게 좋아)
const blacklist = new Set();
// 토큰 생성
app.post('/login', (req, res) => {
const token = jwt.sign({ userId: 123 }, 'secret', { expiresIn: '15m' });
res.json({ token });
});
// 로그아웃
app.post('/logout', (req, res) => {
const token = req.headers.authorization.split(' ')[1];
blacklist.add(token);
res.json({ message: 'Logged out successfully' });
});
// 토큰 검증 미들웨어
function verifyToken(req, res, next) {
const token = req.headers.authorization.split(' ')[1];
if (blacklist.has(token)) {
return res.status(401).json({ error: 'Token is blacklisted' });
}
jwt.verify(token, 'secret', (err, decoded) => {
if (err) {
return res.status(401).json({ error: 'Invalid token' });
}
req.userId = decoded.userId;
next();
});
}
// 보호된 라우트
app.get('/protected', verifyToken, (req, res) => {
res.json({ message: 'Access granted', userId: req.userId });
});
app.listen(3000, () => console.log('Server running on port 3000'));
이 예제에서는 로그아웃 시 토큰을 블랙리스트에 추가하고, 매 요청마다 토큰이 블랙리스트에 있는지 확인해. 이렇게 하면 로그아웃한 토큰의 재사용을 막을 수 있어. 👍
2.5. 민감한 정보 노출 👀
다섯 번째로 알아볼 취약점은 '민감한 정보 노출'이야. JWT의 페이로드는 단순히 Base64Url로 인코딩되어 있을 뿐, 암호화되어 있지 않아. 그래서 누구나 쉽게 디코딩해서 내용을 볼 수 있어. 😱
예를 들어, 이런 JWT가 있다고 해보자:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huQGV4YW1wbGUuY29tIiwicGFzc3dvcmQiOiJzZWNyZXQxMjMifQ.71ZqSuKy_2rVKA9FfEZYHJMXDWqWs9R9BDVwf7qoGCM
이 토큰의 페이로드를 디코딩하면 이렇게 돼:
{
"userId": 123,
"name": "John Doe",
"email": "john@example.com",
"password": "secret123"
}
보이지? 비밀번호까지 그대로 노출되고 있어! 이런 식으로 민감한 정보를 JWT에 포함시키면 보안에 큰 구멍이 생기는 거야. 🕳️
이런 문제를 막으려면 어떻게 해야 할까? 🤔
- 민감한 정보 제외하기: 비밀번호, 신용카드 정보 등 민감한 데이터는 절대로 JWT에 포함시키지 마.
- 필요한 정보만 포함하기: 꼭 필요한 정보만 JWT에 넣어. 사용자 ID 정도면 충분할 거야.
- 클레임 이름 줄이기: 긴 클레임 이름 대신 짧은 이름을 사용해. 예를 들어, "email" 대신 "e"를 쓰는 식으로.
- 중요 정보 암호화하기: 꼭 필요하다면 페이로드의 특정 필드를 별도로 암호화해.
예를 들어, 위의 JWT를 이렇게 수정할 수 있어:
{
"sub": 123,
"iat": 1516239022
}
여기서 "sub"는 subject의 약자로 사용자 ID를 나타내고, "iat"는 issued at의 약자로 토큰이 발급된 시간을 나타내. 이렇게 하면 필요한 정보만 포함하고 민감한 정보는 제외할 수 있어. 👍
민감한 정보를 다룰 때는 항상 주의해야 해. JWT는 편리하지만, 그 내용이 쉽게 노출될 수 있다는 걸 항상 기억해야 돼. 🔒
3. JWT 보안 강화를 위한 베스트 프랙티스 💪
자, 이제 JWT의 주요 취약점들을 알아봤으니, 어떻게 하면 이런 문제들을 예방하고 JWT를 더 안전하게 사용할 수 있을지 정리해볼게. 여기 JWT 보안을 강화하기 위한 베스트 프랙티스들이야. 꼭 기억해둬! 📝
3.1. 강력한 서명 알고리즘 사용하기 🔐
HS256(HMAC with SHA-256)이나 RS256(RSA Signature with SHA-256) 같은 강력한 알고리즘을 사용해. 'none' 알고리즘은 절대 사용하지 마!
예를 들어, Node.js에서는 이렇게 할 수 있어:
const jwt = require('jsonwebtoken');
const token = jwt.sign({ userId: 123 }, 'your-secret-key', { algorithm: 'HS256' });
3.2. 토큰 만료 시간 설정하기 ⏰
JWT에 짧은 만료 시간을 설정해. 이렇게 하면 토큰이 탈취되더라도 공격자가 사용할 수 있는 시간을 제한할 수 있어.
Python에서 PyJWT를 사용한다면 이렇게 할 수 있어:
import jwt
import datetime
token = jwt.encode({
'user_id': 123,
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
}, 'your-secret-key', algorithm='HS256')
이 예제에서는 토큰의 만료 시간을 30분으로 설정했어.
3.3. 안전한 키 관리 🗝️
비밀키를 안전하게 관리하는 게 정말 중요해. 키가 노출되면 모든 보안이 무너질 수 있으니까.
- 환경 변수나 설정 파일을 사용해서 키를 저장해.
- 키를 주기적으로 변경해 (키 순환).
- 가능하다면 하드웨어 보안 모듈(HSM)을 사용해.
예를 들어, Node.js에서 환경 변수를 사용한다면:
// .env 파일
JWT_SECRET=your-very-secret-and-long-random-key
// 애플리케이션 코드
const jwt = require('jsonwebtoken');
require('dotenv').config();
const token = jwt.sign({ userId: 123 }, process.env.JWT_SECRET);
3.4. HTTPS 사용하기 🔒
JWT를 전송할 때는 반드시 HTTPS를 사용해. 이렇게 하면 토큰이 네트워크 상에서 암호화되어 전송되어 중간자 공격을 막을 수 있어.
Node.js의 Express에서 HTTPS를 설정하는 예:
const https = require('https');
const fs = require('fs');
const express = require('express');
const app = express();
const options = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
};
https.createServer(options, app).listen(443);
3.5. 토큰 저장소 사용하기 📦
토큰 블랙리스트나 화이트리스트를 관리하는 저장소를 사용해. 이렇게 하면 로그아웃한 토큰이나 탈취된 토큰을 무효화할 수 있어.
Redis를 사용한 토큰 블랙리스트 예제:
const redis = require('redis');
const client = redis.createClient();
// 토큰을 블랙리스트에 추가
function blacklistToken(token, expirationTime) {
client.set(token, 'blacklisted', 'EX', expirationTime);
}
// 토큰이 블랙리스트에 있는지 확인
function isTokenBlacklisted(token) {
return new Promise((resolve, reject) => {
client.get(token, (err, reply) => {
if (err) reject(err);
resolve(reply === 'blacklisted');
});
});
}
3.6. 적절한 클레임 사용하기 📝
JWT 표준 클레임을 적절히 사용해. 특히 'exp'(만료 시간), 'iat'(발급 시간), 'sub'(주체) 등을 꼭 포함시켜.
Python에서 PyJWT를 사용한 예:
import jwt
import datetime
token = jwt.encode({
'sub': '0',
'name': 'John Doe',
'iat': datetime.datetime.utcnow(),
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}, 'your-secret-key', algorithm='HS256')
3.7. 토큰 갱신 전략 수립하기 🔄
리프레시 토큰을 사용해서 액세스 토큰의 수명을 짧게 유지하면서도 사용자 경험을 해치지 않을 수 있어.
Node.js에서 리프레시 토큰을 사용하는 예:
const jwt = require('jsonwebtoken');
function generateTokens(userId) {
const accessToken = jwt.sign({ userId }, 'access-secret', { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId }, 'refresh-secret', { expiresIn: '7d' });
return { accessToken, refreshToken };
}
function refreshAccessToken(refreshToken) {
try {
const { userId } = jwt.verify(refreshToken, 'refresh-secret');
return jwt.sign({ userId }, 'access-secret', { expiresIn: '15m' });
} catch (error) {
throw new Error('Invalid refresh token');
}
}
3.8. 에러 처리 주의하기 ⚠️
토큰 검증 실패 시 자세한 에러 메시지를 반환하지 마. 공격자에게 유용한 정보를 제공할 수 있어.
Express.js에서의 에러 처리 예:
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
return res.status(401).json({ error: 'Invalid token' });
}
next(err);
});
3.9. 정기적인 보안 감사 실시하기 🕵️♂️
정기적으로 보안 감사를 실시해서 JWT 구현에 문제가 없는지 확인해. 새로운 취약점이 발견될 수 있으니 항상 주의를 기울여야 해.
이런 도구들을 사용해볼 수 있어:
- OWASP ZAP (Zed Attack Proxy)
- Burp Suite
- JWT.io
예를 들어, JWT.io를 사용해서 토큰을 디코딩하고 검증할 수 있어:
// https://jwt.io/ 웹사이트에 접속
// 토큰을 "Encoded" 섹션에 붙여넣기
// "Decoded" 섹션에서 토큰의 내용 확인
// "Verify Signature" 섹션에서 서명 검증
이렇게 하면 토큰의 구조와 내용을 쉽게 확인할 수 있고, 잠재적인 보안 문제를 발견할 수 있어.
4. 결론 🏁
자, 이제 JWT의 보안 취약점과 그에 대한 대응책에 대해 깊이 있게 알아봤어. 정말 많은 내용이었지? 😅
기억해야 할 핵심 포인트를 정리해볼게:
- JWT는 편리하지만 완벽하지 않아. 항상 보안에 주의를 기울여야 해.
- 강력한 알고리즘과 안전한 키 관리가 JWT 보안의 기본이야.
- 토큰의 수명을 제한하고, 필요하다면 리프레시 토큰을 사용해.
- 민감한 정보는 절대로 JWT에 포함시키지 마.
- HTTPS를 항상 사용하고, 토큰 저장소를 활용해.
- 정기적인 보안 감사로 새로운 취약점에 대비해.
JWT를 사용할 때 이런 점들을 잘 기억하고 실천한다면, 훨씬 더 안전한 웹 애플리케이션을 만들 수 있을 거야. 보안은 한 번에 완성되는 게 아니라 계속해서 신경 써야 하는 과정이라는 걸 잊지 마! 🛡️
그리고 마지막으로, 항상 최신 보안 동향을 주시하고 새로운 취약점이 발견되면 빠르게 대응할 준비를 해야 해. 기술은 계속 발전하고, 그에 따라 새로운 위협도 계속 등장하니까.
자, 이제 넌 JWT의 보안 전문가가 된 거야! 이 지식을 잘 활용해서 더 안전한 웹 서비스를 만들어 나가길 바랄게. 화이팅! 💪😊