JWT를 분석하고 사용해보자 (2/3)
해당 게시물은 시리즈이다.
목차
1편 : 개념 / 개요 / 프로토콜 설명
2편 : JWT(JWS) 보안 취약점 / 준수사항 분석
3편 : 실제로 구현해보자
JWT 실제 사용 및 보안적 측면 분석
해당 사항을 시작하기 전, JWT 표준은 보안 논란이 있는 표준이다. 다만 사용범위를 제한하고 안전하게 사용하면 JWT 보안 논란에서 벗어날 수 있을 것이다.
JWT 사용 유의사항
1. Stateless 서비스에만 적용하자(활용 측면)
- stateful Web Service를 할 때 클라이언트와 주고받은 정보는 지속적이면서, 정보의 양도 많고 유동적이면서 가변적이다. 해당 정보를 JWT payload claim에 의지한다는 것 부터 확장성/활용성에 정말 좋지 않다.
- 쉽게 예를 들어보자, 자동 로그인, 장바구니 기능을 개발한다. 그러면 세션/쿠키/DB(자동로그인)를 무조건 사용해야한다. 해당 서비스는 JWT 토큰으로 절대 소화하지 못한다.
- 그러나, Stateless 서비스의 경우 즉, API Web Server 같은 경우, 단발성 요청만 존재한다. 여기서 필요한 것은 식별/인증/인가 그 이상 그 이하도 아니고 상태를 계속 유지할 이유도 필요도 없다.
2. JWS는 읽을 수 있는 데이터이기 때문에 개인정보의 성격이 강한 것은 절대 payload에 적재하지 말자.
3. 무조건 HTTPS 환경에서 사용하자.
- HTTP 환경에서는 전송 계층이 암호화가 되지 않는다. 즉, 토큰 값이 전송계층에서 평문으로 전송되며, 외부에서 패킷 덤프를 통해 토큰을 유출할 수 있다.
- 클라이언트는 JWT 토큰을 어디에 보관할까?
- HTML5 부터 Web Storage라는 개념이 도입되었다. 말그대로 웹 보관함이다. 서버가 아닌, 클라이언트에 데이터를 저장하는 것이다. Web Storage는 LocalStorage와 SessionStorage가 있다.
- LocalStorage 같은 경우, 반영구적으로 보관하는 것인데, 브라우저를 종료해도(세션이 소멸되어도) 데이터는 유지된다. 다른 도메인은 LocalStorage에 접근하지 못한다.
- SessionStorage의 경우, 각 세션마다 데이터가 별개로 저장된다. 세션을 종료하면 데이터는 자동 소멸된다. 같은 도메인이라도 세션이 다르면 접근을 하지 못한다.
- 일단 localStorage에 절대 보관하지말자. 해당 저장소에 보관하면 탈취될 우려가 있다.
- 결론은 Session Storage 또는 Secure Cookie에 보관하자.
- 그러나 보관할 때 주의사항이 있다.
- XSS/CSRF 공격으로부터 세션 스토리지는 안전하지 않다. XSS/CSRF 공격으로부터 안전하다는 가정하에 세션스토리지에 보관해야한다.
- 쿠키를 통해 보관할 때 XSS/CSRF를 통해 쿠키 탈취 또는 HTTP 통신할 때 쿠키가 노출될 우려가 있기 때문에 HTTP Only Cookie 설정과 HttpCookie 클래스의 Secure 속성을 true을 하여, HTTP에서 쿠키 전송을 하지 않게 하자.
- 차라리 세션 스토리지보다 Secure Cookie에 보관하는게 안전할 수도 있다.
4. 대칭키는 쓰지말고 비대칭키를 사용하자
- 일단, 비대칭키를 쓰는 것은 보안성과 확장성의 이점을 갖고 있다.
- 확장성의 이점은 다음과 같다. 만약 당신이 Single Server가 아닌 Muliple 서버를 구축한다. Single Server는 JWT 토큰 발급 서버와 비즈니스 서버가 같은것이며, Multiple 서버는 JWT 토큰 발급 서버 A/B/C 별 비즈니스 서버가 분리된 것이다. 대칭키를 사용할 때 Multiple 서버에서 토큰 검증은 어떻게 할 것인가? A/B/C 비즈니스 서버에게 대칭키를 줄 것인가? 그러면 A/B/C 비즈니스 서버는 토큰을 생성할 수 있고 변조할 수 있다. 만약 비대칭키를 사용하면, PublicKey만 주면 끝이다.
- 보안성의 이점은 다음과 같다. 대칭키를 사용하면 alg(JOSE Header)에 HS256과 같은 HMAC을 생성한다. HMAC의 식은 다음과 같다. HMAC(Key, plaintext, o_pad/i_pad) 짧은 식이지만 결론은 해시를 2번 실행한다. 그러나 해시에는 문제점이 있다. 패스워드를 저장하기 위해 설계된 것이 아니라, 짧은 시간에 데이터를 검색하기 위해 설계된 것이다. 이러한 현상을 위해 설계 된것이기 때문에 해시함수는 빠른 처리 속도를 갖고 있다. 이러한 빠른 처리 속도는 전사 공격(Brute Force)에 취약하며 실제로 해당 공격으로 JWT HS256을 뚫어버린 예가 있다(아래 레퍼런스 참고).
5. 암호키 정책
- JWT 토큰을 만들 때는 암호키를 사용한다. 결국 암호키의 관리는 필수적으로 필요하다. 암호키의 수명은 다양한 케이스를 통해 측정이 가능하다. 예는 다음과 같다. 사용 빈도 / 암호화하는 데이터의 민감도 / 암호문의 노출 빈도 / 암호화 하는 데이터의 양 / 사용 알고리즘 / 암호키 보관함의 보안 강도 / 암호키의 접근하는 사람 수 등에 따라 암호키의 수명을 결정할 수 있다. 또한 당연히 NIST에서 권고하는 암호키 길이(비도)를 적용해야 한다.
- 위에 정책을 토대로 객관화가 힘들면 그냥 반년에 한번 씩 갱신하자(저도 저 위의 주관화를 객관화 못하겠음).
- 암호키의 갱신은 내부적으로 구현한다(스케줄러 같은 것). JWT 토큰은 kid JOSE Header를 통해 암호키의 식별 값을 통해 암호키 갱신을 무리 없이 진행할 수 있다.
- Multiple Service일 경우, 서비스 별 다른 암호키를 적용해라.
6. jku를 통해 공개키 정보에 대해 링크를 걸자
- 다음은 JWK 규격에 맞게 암호키를 표현한 것이다.
- https://username.auth0.com/.well-known/jwks.json
7. Algo:none/algo:RS256 to HS256 과 같은 alg에 의존하는 라이브러리르 사용하지 말자
- JWT 토큰 관련 라이브러리가 alg 값에 의존하면 검증 절차를 제대로 진행하지 않는다.
- alg:none을 했을 경우, 잘못된 라이브러리는 alg값이 없기 떄문에 검증 자체를 무시한다.
- alg:RS256 의 값을 alg:HS256으로 수정했을 경우, 잘못된 라이브러리는 HS256 검증을 해버린다. 공격자가 공개키를 이용하여 HS256을 해버리고 Signature 값을 변조하고 Request를 한다. JWT 토큰 발급 서버 또한 공개키를 이용하여 HS256연산을 하고 공격은 성공이된다.
8. Payload에 exp/nbf/iss/aud를 검증하고 꼭 적재하자.
- exp: 토큰의 만료 기간이다.
- JWT 토큰에 최대 단점이자 취약점은 발급서버에서 토큰을 관리하지 못하는 것이다. 인증서의 경우 CRL이 존재하여, 폐기한 인증서는 CRL을 통해 확인할 수 있지만, JWT는 그런 것이 없다. 만약 토큰이 탈취 당하면 해당 만료 기간까지 공격자가 마음껏 사용할 수 있다.
- 그러면 서버에서 토큰을 관리하며, TRL(Token Rovoke List)을 만들면 되지 않냐?? 그렇게 얘기하면 일단 이 행위는 JWT 규격 외의 행동이며, JWT 사용 목적과 의의가 사라진다.
- nbf: Not Before(인증서의 Not Before와 같은 개념) 즉, 해당 값 이 후부터 토큰을 사용할 수 있다. 토큰의 기간 검증은 nbf < current Time < exp 조건이 만족해야 한다.
- Iss: 토큰의 발급자가 누구인지 지정을 하며, 해당 값을 검증을 해야한다.
- aud: sub랑 헷갈릴 수 있지만, 해당 토큰을 사용하는 대상 서비스라고 생각하면 되겠다. Multiple service에서 aud 검증은 꼭 필요하다. A 서비스를 위한 토큰을 발급했는데 aud 값이 없을 경우, B 서비스에서도 성곡적으로 사용할 수 있는 문제가 있다.
위와 같은 많은 문제점이 있는데 JWT 토큰 포멧 구성은 어떻게 할지 정말 걱정이 많을 것이다. 위의 유의사항을 고려한 포멧 구성을 아래와 같이 진행한다. 포멧 구성은 필자의 절대적인 주관이다(=틀릴수도 있다구요).
JWT 포멧 구성 예
HEADER
{
“alg”:”RS256”,
“kid”:”RTAwMzhEQTY2RjRGMDM3NzBGNDc4NDZFMjQ1MUY5RUM0RkIzNDkwMQ”,
“jwu”:https://username.auth0.com/.well-known/jwks.json,
“typ”:”jwt”
}
PAYLOAD
{
“iss”:auth.glaso.net”,
“aud”:”cert.glaso.net”,
“nbf”:”1564117471”,
“exp”:”1564160640”,
“sub”:”ehdvudee”,
“scope”:[
“admin”,
“devel_user”
],
“name”:”shin”,
“lad”:”1564003121”
}
설명
RSA SHA256 서명을 하는 JWT이다. Kid는 인증서의 지문 값으로 하였다(x5t). 해당 kid는 어떤 암호키를 이용하여 서명 값을 생성했는지 알려준다. 또한 암호키 갱신할 때 사용한다. jwu는 클라이언트가 토큰 검증을 할 때 암호키의 정보를 받아올 수 있는 링크이다. Iss/aud/nbf/exp는 위에 유의사항에서 언급한 것이며, sub는 토큰을 사용하는 자의 아이디 값이며, scope은 해당 토큰의 인가 값이다. 여기까지의 페이로드는 registered claim이며, name, lad는 필자가 생성한 private claim이다. 토큰 생성자의 이름과 마지막 접속일을 뜻한다.
Reference
JWT 관련
1. https://bcho.tistory.com/999
5. https://medium.facilelogin.com/jwt-jws-and-jwe-for-not-so-dummies-b63310d201a3#39b4
6. https://blog.larapulse.com/web/jwt
7. JWT HANDBOOK Sebastian Peyrott
JWT 보안 관련
1. https://www.pingidentity.com/en/company/blog/posts/2019/jwt-security-nobody-talks-about.html
2. https://cheatsheets.pragmaticwebsecurity.com/jwt.html
3. https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage
4. https://nsinc.tistory.com/121
5. https://tools.ietf.org/html/draft-ietf-oauth-jwt-bcp-06
6. https://github.com/shieldfy/API-Security-Checklist/issues/6
7. http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/
9. https://cheatsheets.pragmaticwebsecurity.com/jwt.html
유용한 사이트
1. JOSE Header List: https://www.iana.org/assignments/jose/jose.xhtml
2. JWT Payload List: https://www.iana.org/assignments/jwt/jwt.xhtml
3. JWT Debugger: https://jwt.io/
RFC 문서