- Published on
JWT none alg 취약점 재현·차단 실전
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서명 기반 인증을 JWT로 구현할 때, 가장 유명하면서도 지금도 종종 재발하는 실수가 alg를 신뢰해버리는 것입니다. 공격자는 JWT 헤더의 alg를 none으로 바꾸고 서명을 제거해도, 서버가 이를 “서명 없는 토큰”으로 받아들이면 인증이 뚫립니다. 이 글에서는 none 알고리즘 취약점을 재현 가능한 형태로 보여주고, 프레임워크/라이브러리별 차단 포인트와 운영에서의 방어선까지 정리합니다.
전제: 아래 내용은 보안 점검/교육 목적입니다. 실제 서비스에 대한 무단 테스트는 금지하세요.
1) none alg 취약점이 생기는 조건
JWT는 header.payload.signature 구조이며, 헤더에 alg(알고리즘)와 typ 등이 들어갑니다.
- 정상 예:
alg가HS256또는RS256등이며 서버는 서명을 검증해야 함 - 취약 예: 서버가
- 헤더의
alg를 그대로 신뢰하거나 alg가none인 토큰을 “검증 성공”으로 처리하거나- 검증 함수를 “디코딩” 함수로 착각해 쓰는 경우
- 헤더의
특히 다음 패턴이 위험합니다.
- “토큰 파싱”과 “토큰 검증”을 혼동
- 검증 시 허용 알고리즘 목록을 명시하지 않음
- 키가 없으면(또는 비어 있으면) 검증을 건너뜀
2) 로컬에서 취약점 재현하기 (Node.js 예제)
아래는 의도적으로 취약한 서버 예제입니다. 핵심은 jsonwebtoken의 decode를 verify처럼 사용하거나, verify에 잘못된 옵션을 주는 실수입니다.
2.1 취약한 구현: decode로 인증해버리기
// app.js
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
app.get('/admin', (req, res) => {
const auth = req.headers.authorization || '';
const token = auth.startsWith('Bearer ') ? auth.slice(7) : null;
if (!token) return res.status(401).send('no token');
// 취약: 서명 검증 없이 payload만 꺼냄
const payload = jwt.decode(token);
if (!payload) return res.status(401).send('bad token');
if (payload.role === 'admin') return res.send('welcome admin');
return res.status(403).send('forbidden');
});
app.listen(3000, () => console.log('listening on 3000'));
설명:
jwt.decode는 Base64URL 디코딩만 수행합니다.- 따라서 공격자는
role=admin을 마음대로 넣을 수 있습니다.
이 경우는 none이 아니더라도 뚫리지만, 현장에서 none 공격은 “서명 없는 토큰도 통과한다”는 점검 시그널로 자주 등장합니다.
2.2 none 토큰 직접 만들기
JWT는 Base64URL 인코딩을 쓰므로, 헤더/페이로드를 만들고 서명을 비워 header.payload. 형태로 만들 수 있습니다.
// make-none-jwt.js
function b64url(obj) {
return Buffer.from(JSON.stringify(obj))
.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}
const header = { alg: 'none', typ: 'JWT' };
const payload = {
sub: 'attacker',
role: 'admin',
iat: Math.floor(Date.now() / 1000),
};
const token = `${b64url(header)}.${b64url(payload)}.`;
console.log(token);
실행:
node make-none-jwt.js
요청:
curl -H "Authorization: Bearer $(node make-none-jwt.js)" http://localhost:3000/admin
취약 서버라면 welcome admin이 떨어집니다.
3) 차단 1순위: “검증 함수”만 쓰고 허용 알고리즘을 고정
가장 중요한 원칙은 두 가지입니다.
- 반드시 서명 검증을 수행할 것 (
decode가 아닌verify) - 허용 알고리즘 allowlist를 코드로 고정할 것
3.1 Node.js jsonwebtoken 안전한 예
const jwt = require('jsonwebtoken');
function verifyAccessToken(token) {
// HS256 예시: 서버만 아는 비밀키 사용
const secret = process.env.JWT_SECRET;
return jwt.verify(token, secret, {
algorithms: ['HS256'],
issuer: 'your-issuer',
audience: 'your-audience',
});
}
포인트:
algorithms를 명시하면none은 기본적으로 거부됩니다.issuer,audience까지 검증하면 토큰 재사용/오발급도 줄일 수 있습니다.
3.2 RS256 사용 시 추가 체크
RS256은 공개키/개인키 기반이라 키 관리가 더 명확해집니다. 다만 구현 실수로 “대칭키처럼” 검증하거나, kid 헤더를 신뢰해 임의 URL에서 JWK를 받아오는 패턴이 또 다른 취약점이 됩니다.
kid기반 JWK 로딩은 고정된 JWKS URL에서만, 캐시와 검증을 갖춰야 함- 허용 알고리즘은
['RS256']처럼 고정
4) 차단 포인트: Spring Security / Java 계열
Spring Security에서 JWT를 다룰 때도 핵심은 같습니다.
- 디코딩만 하지 말고 검증이 포함된 디코더를 사용
- 허용 알고리즘을 제한
예를 들어 Resource Server를 쓴다면, Nimbus 기반 검증이 들어가며 none은 기본적으로 막히는 편이지만, 커스텀 구현에서 SignedJWT.parse 후 검증을 생략하면 동일한 문제가 발생합니다.
Spring Security 인증 흐름을 구성하다 보면 리다이렉트/필터 체인 이슈로 “임시로 검증을 느슨하게” 하는 일이 생기는데, 이런 변경이 보안 사고로 이어지기 쉽습니다. 인증 플로우가 꼬였을 때의 디버깅 관점은 아래 글도 참고할 만합니다.
5) 운영 방어선: 게이트웨이/인그레스에서 1차 차단
애플리케이션 레벨에서 완벽히 막는 것이 우선이지만, 운영에서는 게이트웨이에서 alg=none 패턴을 조기 차단하는 것도 유용합니다.
다만 JWT는 Base64URL로 인코딩되어 있어, 단순 문자열 매칭이 항상 통하지는 않습니다. 그래도 다음은 현실적인 방어선입니다.
- API Gateway/WAF에서
Authorization헤더 길이/형식 검증 header디코딩 후alg검사(지원 기능이 있는 제품에 한함)- “서명 없는 토큰” 형태인
header.payload.(마지막 점으로 끝남) 같은 비정상 패턴 차단
Kubernetes 환경이라면 인그레스/게이트웨이 설정 오류로 인증 우회가 발생하는 경우도 있습니다. 운영 장애와 함께 보안 설정이 무너지는 상황을 막기 위해, 크래시 루프나 설정 롤백 시나리오까지 포함해 점검하는 것이 좋습니다.
6) 점검 체크리스트 (실무용)
다음 체크리스트로 none 취약점을 빠르게 점검할 수 있습니다.
6.1 코드/설정
decode/parse만 하고verify/validate를 안 하는 코드가 있는가- 허용 알고리즘이 allowlist로 고정되어 있는가
alg를 요청 토큰 헤더에서 읽어 동적으로 검증 로직을 바꾸는가- 키가 없거나 예외가 발생했을 때 “일단 통과”시키는 fallback이 있는가
iss,aud,exp,nbf검증이 켜져 있는가
6.2 테스트 케이스
alg를none으로 하고 서명 제거한 토큰이 통과하는가alg를HS256으로 바꾸고 임의 서명을 넣었을 때 통과하는가- 만료(
exp) 지난 토큰이 통과하는가 - 다른 서비스의 토큰(
aud다름)이 통과하는가
6.3 로깅/관측
- JWT 검증 실패 사유가 로깅되는가(민감정보 제외)
kid,iss,aud불일치가 관측되는가- 특정 IP/UA에서 반복적으로 토큰 형식이 깨진 요청이 오는가
7) 보너스: CI에서 재발 방지(간단한 자동화)
재발 방지는 “사람이 기억”하는 게 아니라 “테스트가 막”아야 합니다. 아래는 alg=none 토큰이 401 또는 403으로 떨어지는지 확인하는 초간단 스모크 테스트 예시입니다.
# none 토큰으로 /admin 접근 시 200이 나오면 실패
TOKEN=$(node make-none-jwt.js)
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $TOKEN" http://localhost:3000/admin)
if [ "$STATUS" = "200" ]; then
echo "FAIL: none alg token accepted"
exit 1
fi
echo "PASS: none alg token rejected with $STATUS"
GitHub Actions에서 캐시/환경 변수 실수로 보안 테스트가 스킵되는 경우도 있으니, 파이프라인 신뢰성도 같이 챙기는 게 좋습니다.
8) 정리
none alg 취약점은 “JWT를 쓴다”는 사실만으로 안전해지지 않는다는 대표 사례입니다. 결론은 단순합니다.
- 검증은
verify로만 하고 디코딩 결과를 신뢰하지 말 것 - 허용 알고리즘을 고정하고,
iss/aud/만료까지 검증할 것 - 운영에서는 게이트웨이/WAF로 비정상 토큰을 조기 차단하고
- CI에
none토큰 거부 테스트를 넣어 재발을 막을 것
이 4가지만 지켜도 alg=none 류의 실수는 대부분 예방할 수 있습니다.