JWT(JSON 웹 토큰) 보안 모범 사례에 대한 종합 가이드. 국제적인 애플리케이션의 일반적인 취약점에 대한 검증, 저장, 서명 알고리즘, 완화 전략을 다룹니다.
JWT 토큰: 글로벌 애플리케이션을 위한 보안 모범 사례
JSON 웹 토큰(JWT)은 두 당사자 간에 클레임을 안전하게 표현하기 위한 표준 방법이 되었습니다. JWT의 컴팩트한 구조, 사용 용이성, 다양한 플랫폼에서의 폭넓은 지원 덕분에 최신 웹 애플리케이션, API, 마이크로서비스에서 인증 및 인가를 위한 인기 있는 선택지가 되었습니다. 그러나 널리 채택되면서 보안 취약점에 대한 정밀한 조사가 증가하고 수많은 취약점이 발견되었습니다. 이 종합 가이드에서는 글로벌 애플리케이션이 잠재적인 공격에 대해 안전하고 복원력을 유지하도록 보장하기 위한 JWT 보안 모범 사례를 살펴봅니다.
JWT란 무엇이며 어떻게 작동하는가?
JWT는 세 부분으로 구성된 JSON 기반 보안 토큰입니다:
- 헤더(Header): 토큰의 유형(JWT)과 사용된 서명 알고리즘(예: HMAC SHA256 또는 RSA)을 지정합니다.
- 페이로드(Payload): 엔티티(일반적으로 사용자)에 대한 설명과 추가 메타데이터인 클레임(claims)을 포함합니다. 클레임은 등록(예: 발급자, 주체, 만료 시간), 공개(애플리케이션에 의해 정의됨) 또는 비공개(사용자 정의 클레임)가 될 수 있습니다.
- 서명(Signature): 인코딩된 헤더, 인코딩된 페이로드, 비밀 키(HMAC 알고리즘의 경우) 또는 개인 키(RSA/ECDSA 알고리즘의 경우), 지정된 알고리즘을 결합하고 그 결과를 서명하여 생성됩니다.
이 세 부분은 Base64 URL로 인코딩되고 점(.
)으로 연결되어 최종 JWT 문자열을 형성합니다. 사용자가 인증하면 서버는 JWT를 생성하고, 클라이언트는 이를 저장(일반적으로 로컬 스토리지나 쿠키에)한 후 후속 요청에 포함시킵니다. 그러면 서버는 JWT를 검증하여 요청을 인가합니다.
일반적인 JWT 취약점 이해하기
모범 사례를 살펴보기 전에 JWT와 관련된 일반적인 취약점을 이해하는 것이 중요합니다:
- 알고리즘 혼동(Algorithm Confusion): 공격자는
alg
헤더 매개변수를 강력한 비대칭 알고리즘(예: RSA)에서 약한 대칭 알고리즘(예: HMAC)으로 변경하는 기능을 악용합니다. 서버가 HMAC 알고리즘에서 공개 키를 비밀 키로 사용하면 공격자는 JWT를 위조할 수 있습니다. - 비밀 키 노출(Secret Key Exposure): JWT 서명에 사용된 비밀 키가 유출되면 공격자는 유효한 JWT를 생성하여 모든 사용자를 사칭할 수 있습니다. 이는 코드 유출, 안전하지 않은 저장소 또는 애플리케이션의 다른 부분의 취약점으로 인해 발생할 수 있습니다.
- 토큰 탈취(XSS/CSRF): JWT가 안전하지 않게 저장된 경우, 공격자는 사이트 간 스크립팅(XSS) 또는 사이트 간 요청 위조(CSRF) 공격을 통해 이를 탈취할 수 있습니다.
- 재전송 공격(Replay Attacks): 공격자는 유효한 JWT를 재사용하여 무단 접근을 시도할 수 있으며, 특히 토큰의 수명이 길고 특정 대응책이 구현되지 않은 경우에 더욱 그렇습니다.
- 패딩 오라클 공격(Padding Oracle Attacks): JWT가 특정 알고리즘으로 암호화되고 패딩이 잘못 처리될 경우, 공격자는 잠재적으로 JWT를 해독하여 그 내용을 접근할 수 있습니다.
- 클럭 스큐 문제(Clock Skew Issues): 분산 시스템에서 서로 다른 서버 간의 클럭 스큐(시간 불일치)는 특히 만료 클레임과 관련하여 JWT 검증 실패로 이어질 수 있습니다.
JWT 보안 모범 사례
다음은 JWT와 관련된 위험을 완화하기 위한 포괄적인 보안 모범 사례입니다:
1. 올바른 서명 알고리즘 선택
서명 알고리즘의 선택은 매우 중요합니다. 고려해야 할 사항은 다음과 같습니다:
alg: none
피하기:alg
헤더를none
으로 설정하는 것을 절대 허용하지 마십시오. 이는 서명 검증을 비활성화하여 누구나 유효한 JWT를 만들 수 있게 합니다. 많은 라이브러리가 이를 방지하도록 패치되었지만, 사용 중인 라이브러리가 최신 버전인지 확인하십시오.- 비대칭 알고리즘(RSA/ECDSA) 선호: 가능하면 RSA(RS256, RS384, RS512) 또는 ECDSA(ES256, ES384, ES512) 알고리즘을 사용하십시오. 비대칭 알고리즘은 서명에는 개인 키를, 검증에는 공개 키를 사용합니다. 이는 공격자가 공개 키에 접근하더라도 토큰을 위조하는 것을 방지합니다.
- 개인 키 안전하게 관리: 하드웨어 보안 모듈(HSM)이나 안전한 키 관리 시스템을 사용하여 개인 키를 안전하게 저장하십시오. 소스 코드 저장소에 개인 키를 절대 커밋하지 마십시오.
- 정기적으로 키 교체: 서명 키를 정기적으로 변경하는 키 교체 전략을 구현하십시오. 이는 키가 유출되었을 경우의 영향을 최소화합니다. 공개 키를 게시하기 위해 JSON 웹 키 세트(JWKS) 사용을 고려하십시오.
예시: 키 교체를 위한 JWKS 사용
JWKS 엔드포인트는 JWT를 검증하는 데 사용할 수 있는 공개 키 세트를 제공합니다. 서버는 키를 교체할 수 있으며, 클라이언트는 JWKS 엔드포인트를 가져와 키 세트를 자동으로 업데이트할 수 있습니다.
/.well-known/jwks.json
:
{
"keys": [
{
"kty": "RSA",
"kid": "key1",
"alg": "RS256",
"n": "...",
"e": "AQAB"
},
{
"kty": "RSA",
"kid": "key2",
"alg": "RS256",
"n": "...",
"e": "AQAB"
}
]
}
2. JWT 올바르게 검증하기
올바른 검증은 공격을 방지하는 데 필수적입니다:
- 서명 검증: 항상 올바른 키와 알고리즘을 사용하여 JWT 서명을 검증하십시오. JWT 라이브러리가 올바르게 구성되고 최신 상태인지 확인하십시오.
- 클레임 검증:
exp
(만료 시간),nbf
(유효 시작 시간),iss
(발급자),aud
(수신자)와 같은 필수 클레임을 검증하십시오. exp
클레임 확인: JWT가 만료되지 않았는지 확인하십시오. 공격자의 기회 창을 최소화하기 위해 합리적인 토큰 수명을 구현하십시오.nbf
클레임 확인: JWT가 유효 시작 시간 전에 사용되지 않도록 하십시오. 이는 토큰이 사용될 의도가 있기 전에 재전송 공격을 방지합니다.iss
클레임 확인: JWT가 신뢰할 수 있는 발급자에 의해 발급되었는지 확인하십시오. 이는 승인되지 않은 당사자가 발급한 JWT가 사용되는 것을 방지합니다.aud
클레임 확인: JWT가 귀하의 애플리케이션을 대상으로 하는지 확인하십시오. 이는 다른 애플리케이션을 위해 발급된 JWT가 귀하의 애플리케이션에 대해 사용되는 것을 방지합니다.- 거부 목록 구현 (선택 사항): 중요한 애플리케이션의 경우, 유출된 JWT를 만료 시간 전에 무효화하기 위해 거부 목록(폐기 목록이라고도 함)을 구현하는 것을 고려하십시오. 이는 복잡성을 더하지만 보안을 크게 향상시킬 수 있습니다.
예시: 코드에서 클레임 검증하기 (Node.js와 jsonwebtoken
)
const jwt = require('jsonwebtoken');
try {
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'https://example.com',
audience: 'https://myapp.com'
});
console.log(decoded);
} catch (error) {
console.error('JWT 검증 실패:', error);
}
3. 클라이언트 측에 JWT 안전하게 저장하기
클라이언트 측에서 JWT를 어떻게 저장하는지는 보안에 큰 영향을 미칩니다:
- 로컬 스토리지 피하기: 로컬 스토리지에 JWT를 저장하면 XSS 공격에 취약해집니다. 공격자가 애플리케이션에 자바스크립트를 주입할 수 있다면 로컬 스토리지에서 JWT를 쉽게 훔칠 수 있습니다.
- HTTP-Only 쿠키 사용:
Secure
및SameSite
속성을 가진 HTTP-only 쿠키에 JWT를 저장하십시오. HTTP-only 쿠키는 자바스크립트로 접근할 수 없어 XSS 위험을 완화합니다.Secure
속성은 쿠키가 HTTPS를 통해서만 전송되도록 보장합니다.SameSite
속성은 CSRF 공격을 방지하는 데 도움이 됩니다. - 리프레시 토큰 고려: 리프레시 토큰 메커니즘을 구현하십시오. 단기 액세스 토큰은 즉각적인 인가에 사용되고, 장기 리프레시 토큰은 새로운 액세스 토큰을 얻는 데 사용됩니다. 리프레시 토큰은 안전하게(예: 데이터베이스에 암호화하여) 저장하십시오.
- CSRF 보호 구현: 쿠키를 사용할 때는 동기화 토큰이나 이중 제출 쿠키 패턴과 같은 CSRF 보호 메커니즘을 구현하십시오.
예시: HTTP-Only 쿠키 설정하기 (Node.js와 Express)
app.get('/login', (req, res) => {
// ... 인증 로직 ...
const token = jwt.sign({ userId: user.id }, privateKey, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId: user.id }, refreshPrivateKey, { expiresIn: '7d' });
res.cookie('accessToken', token, {
httpOnly: true,
secure: true, // 프로덕션 환경에서는 true로 설정
sameSite: 'strict', // 또는 필요에 따라 'lax'
maxAge: 15 * 60 * 1000 // 15분
});
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true, // 프로덕션 환경에서는 true로 설정
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7일
});
res.send({ message: '로그인 성공' });
});
4. 알고리즘 혼동 공격으로부터 보호하기
알고리즘 혼동은 심각한 취약점입니다. 이를 방지하는 방법은 다음과 같습니다:
- 허용된 알고리즘 명시적으로 지정하기: JWT를 검증할 때 허용된 서명 알고리즘을 명시적으로 지정하십시오. JWT 라이브러리가 자동으로 알고리즘을 결정하도록 의존하지 마십시오.
alg
헤더를 신뢰하지 않기: JWT의alg
헤더를 맹목적으로 신뢰하지 마십시오. 항상 미리 정의된 허용 알고리즘 목록과 대조하여 검증하십시오.- 강력한 정적 타이핑 사용 (가능하다면): 정적 타이핑을 지원하는 언어에서는 키와 알고리즘 매개변수에 대해 엄격한 타입 검사를 강제하십시오.
예시: 알고리즘 혼동 방지하기 (Node.js와 jsonwebtoken
)
const jwt = require('jsonwebtoken');
try {
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'] // RS256만 명시적으로 허용
});
console.log(decoded);
} catch (error) {
console.error('JWT 검증 실패:', error);
}
5. 적절한 토큰 만료 및 갱신 메커니즘 구현하기
토큰 수명은 주요 보안 고려 사항입니다:
- 단기 수명 액세스 토큰 사용: 액세스 토큰의 수명을 짧게(예: 5-30분) 유지하십시오. 이는 토큰이 유출되었을 경우의 영향을 제한합니다.
- 리프레시 토큰 구현: 사용자가 재인증할 필요 없이 새로운 액세스 토큰을 얻기 위해 리프레시 토큰을 사용하십시오. 리프레시 토큰은 수명이 더 길 수 있지만 안전하게 저장해야 합니다.
- 리프레시 토큰 교체 구현: 새로운 액세스 토큰이 발급될 때마다 리프레시 토큰을 교체하십시오. 이는 이전 리프레시 토큰을 무효화하여 리프레시 토큰이 유출될 경우의 잠재적 피해를 제한합니다.
- 세션 관리 고려: 민감한 애플리케이션의 경우 JWT 외에 서버 측 세션 관리를 구현하는 것을 고려하십시오. 이를 통해 접근 권한을 더 세밀하게 취소할 수 있습니다.
6. 토큰 탈취로부터 보호하기
토큰 탈취를 방지하는 것은 매우 중요합니다:
- 엄격한 콘텐츠 보안 정책(CSP) 구현: XSS 공격을 방지하기 위해 CSP를 사용하십시오. CSP를 사용하면 웹사이트에서 리소스(스크립트, 스타일, 이미지 등)를 로드할 수 있는 소스를 지정할 수 있습니다.
- 사용자 입력 정제: XSS 공격을 방지하기 위해 모든 사용자 입력을 정제하십시오. 신뢰할 수 있는 HTML 정제 라이브러리를 사용하여 잠재적으로 악의적인 문자를 이스케이프 처리하십시오.
- HTTPS 사용: 클라이언트와 서버 간의 통신을 암호화하기 위해 항상 HTTPS를 사용하십시오. 이는 공격자가 네트워크 트래픽을 도청하고 JWT를 훔치는 것을 방지합니다.
- HSTS(HTTP Strict Transport Security) 구현: HSTS를 사용하여 브라우저가 귀하의 웹사이트와 통신할 때 항상 HTTPS를 사용하도록 지시하십시오.
7. 모니터링 및 로깅
효과적인 모니터링과 로깅은 보안 사고를 감지하고 대응하는 데 필수적입니다:
- JWT 발급 및 검증 로깅: 사용자 ID, IP 주소, 타임스탬프를 포함하여 모든 JWT 발급 및 검증 이벤트를 기록하십시오.
- 의심스러운 활동 모니터링: 여러 번의 로그인 시도 실패, 여러 위치에서 동시에 사용되는 JWT, 빠른 토큰 갱신 요청과 같은 비정상적인 패턴을 모니터링하십시오.
- 경고 설정: 잠재적인 보안 사고를 알리는 경고를 설정하십시오.
- 정기적으로 로그 검토: 의심스러운 활동을 식별하고 조사하기 위해 정기적으로 로그를 검토하십시오.
8. 속도 제한
무차별 대입 공격(brute-force attack) 및 서비스 거부(DoS) 공격을 방지하기 위해 속도 제한을 구현하십시오:
- 로그인 시도 제한: 단일 IP 주소 또는 사용자 계정에서의 실패한 로그인 시도 횟수를 제한하십시오.
- 토큰 갱신 요청 제한: 단일 IP 주소 또는 사용자 계정에서의 토큰 갱신 요청 횟수를 제한하십시오.
- API 요청 제한: 단일 IP 주소 또는 사용자 계정에서의 API 요청 횟수를 제한하십시오.
9. 최신 상태 유지하기
- 라이브러리 업데이트 유지: 보안 취약점을 패치하기 위해 JWT 라이브러리 및 종속성을 정기적으로 업데이트하십시오.
- 보안 모범 사례 따르기: JWT와 관련된 최신 보안 모범 사례 및 취약점에 대한 정보를 계속 확인하십시오.
- 보안 감사 수행: 잠재적인 취약점을 식별하고 해결하기 위해 애플리케이션의 보안 감사를 정기적으로 수행하십시오.
JWT 보안을 위한 글로벌 고려 사항
글로벌 애플리케이션에 JWT를 구현할 때 다음을 고려하십시오:
- 시간대: 서버가 신뢰할 수 있는 시간 소스(예: NTP)에 동기화되었는지 확인하여 JWT 검증, 특히
exp
및nbf
클레임에 영향을 줄 수 있는 클럭 스큐 문제를 방지하십시오. 일관되게 UTC 타임스탬프를 사용하는 것을 고려하십시오. - 데이터 프라이버시 규정: GDPR, CCPA 등과 같은 데이터 프라이버시 규정을 유념하십시오. JWT에 저장되는 개인 데이터의 양을 최소화하고 관련 규정을 준수하도록 하십시오. 필요한 경우 민감한 클레임을 암호화하십시오.
- 국제화(i18n): JWT 클레임의 정보를 표시할 때 사용자의 언어 및 지역에 맞게 데이터가 적절하게 현지화되었는지 확인하십시오. 여기에는 날짜, 숫자, 통화를 적절하게 형식화하는 것이 포함됩니다.
- 법률 준수: 여러 국가의 데이터 저장 및 전송과 관련된 법적 요구 사항을 인지하십시오. JWT 구현이 모든 해당 법률 및 규정을 준수하는지 확인하십시오.
- 교차 출처 리소스 공유(CORS): 애플리케이션이 다른 도메인의 리소스에 접근할 수 있도록 CORS를 올바르게 구성하십시오. 이는 여러 서비스 또는 애플리케이션 간에 인증을 위해 JWT를 사용할 때 특히 중요합니다.
결론
JWT는 인증 및 인가를 처리하는 편리하고 효율적인 방법을 제공하지만, 잠재적인 보안 위험도 동반합니다. 이러한 모범 사례를 따르면 취약점의 위험을 크게 줄이고 글로벌 애플리케이션의 보안을 보장할 수 있습니다. 최신 보안 위협에 대한 정보를 계속 확인하고 그에 따라 구현을 업데이트하는 것을 잊지 마십시오. JWT 수명 주기 전반에 걸쳐 보안을 우선시하면 사용자와 데이터를 무단 접근으로부터 보호하는 데 도움이 될 것입니다.