boojongmin / memo

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

jwt 보다 session이 안전할까? 🤔

boojongmin opened this issue · comments

요약

안전하다는 이유만으로 세션 방식을 선택했다면 높은 확률로 틀린거라고 할 수 있다.


들어가기 앞서 용어 정리

stateless와 stateful: 상태값을 서버에서 저장하고 있다는 의미. HTTP는 무상태(stateless) 프로토콜이므로 상태를 유지하기 위해 모든 request에 cookie를 보낸다.

세션: 여러가지 의미로 쓰일수 있으나 여기서는 서버 세션. servlet의 HttpSession은 사용자 식별을 위해 최초 접속시 난수값을 생성하여 서버가 접근할 수 있는 저장소에 저장하고 사용자 식별을 위한 고유값을 response헤더에 set cookie (보통)JSESSIONID란 쿠키를 set하게하고 이후 클라이언트는 이 JSESSIONID를 모든 request에 이 쿠키값을 전달해서 서버는 쿠키를 읽어서 session 저장소에 저장된 여러 난수값중 매칭되는 값을 찾아내고 이를 통해 해당 리퀘스트의 요청자를 식별할 수 있게됨.

cookiejar: 딱히 표준이라고 글로 명시되어 있는건 아닌 것같고 보통 이런역할을 하는 library나 기능을 cookiejar라고 부르는 관례가 있다는 것을 경험적으로 알고 있는데. 보통 cookiejar의 역할은 응답에서 set cookie 부분을 해석해서 쿠키를 검증, 저장하고 모든 리퀘스트에 쿠키를 검증해서 expired 되지 않았다면 서버에 보낼 요청에 자동으로 cookie를 첨부 시켜주는 기능을 해주는 녀석을 cookiejar라고 보통 부르더란. cookie를 담는 항아리란 의미로 적당히 그런 용도의 이름으로 추상화되었다고 보인다.

bearer token: Authorization 헤더에 "Bearer ${토큰}" 이런식으로 보내는데 oauth의 access token을 헤더의 ${토큰} 영역에 담아서 요청에 사용한다. 참고로 access_token은 서버에서 쿠키처럼 액세스토큰 저장소에 저장해서 상태(stateful) 형태로 구현할수 있고 jwt 토큰을 써서 무상태로 구현할 수 있다.

JWT: json web token의 약자로 header, payload, signature로 구성되어 있다. base64로 인코딩 되어있기 때문에 디코딩하면 사용자 식별정보들을 볼 수 있다. 악의적인 변조를 막기 위해 signature 영역은 암호화 알고리즘을 적용해서 서버에서는 사용자로부터 받은 request의 유효성을 확인하기 위해 signature 부분을 통해 validate를 진행하고 성공시 payload 영역을 base64 디코딩하고 json 문자열을 파싱해서 사용자 식별을 위한(java로 예를 들면 Principal이 될 수 있겠다.) 객체를 만들고 이를 통해 authorization을 진행
jwt를 눈으로 보기 좋은곳
image

authentication, authorization : 좋은 자료 많음. 검색해주세요.


본문

필자는 jwt와 세션에 대한 기술 검토에서 핵심은 무상태(stateless)와 상태(stateful)이라는 것이라고 생각한다.
이에 대한 이야기는 후술하고

브라우저의 session cookie는 탈취가 어렵고 html5 이후 등장한 session/localStorage에 있는 jwt로된 access token같은건 탈취가 쉽기 때문에 보안상 약하다고 말하는건 일단 너무 단편적인 의사 결정이라고 생각이 들어서 좀 정리가 필요하다는 생각이 들었다.

일단 session cookie든 jwt로된 토큰이든 HTTP프로토콜 내에서 동작하는 형태이기 때문에 text base라는건 변함이 없다. 즉 네트워크 레이어를 해커가 훔칠수 있다면 session cookie든 HTTP 전문을 훔치고 쿠키튼 헤더든 값을 가져와서 자신이 만든 HTTP전문에 담으면 서버 입장에서는 요청을 보낸자가 정상적인 유저인지 해커인지 알 수가 없다.(그래서 보안을 위해 다양한 것들을 추가하겠지만 일단 가장 기본 영역만 바라봤다.)

즉 HTTP 프로토콜의 특성상 세션이든 JWT 토큰이든 탈취에 용이한 구조다라는건 똑같다는 것이다.

그럼 또 다른걸로는 스크립트 인젝션을 통해서 browser에서 값을 끄집어 내기 쉽다는 블라블라는하는데

즉 해커가 심은 스크립트를 통해 localstorage.getItem('access_token')을 하면 값이 스크립트로 그대로 유출된다는 것이다.
그럼 이렇게 생각한다면
그럼 브라우저를 키고 f12를 눌러서 개발자 콘솔에서 document.cookie를 입력해보기를 권한다.

image

image

위의 이미지처럼 쿠키jar도 결국은 브라우저의 저장소에서 관리되는 데이터일뿐이다.

자 그럼 별거 아닌건데 아는척은 그만하고

둘다 보안상 프론트 레벨에서는 별반 차이가 없다는거다.

그러므로 이제는 앱이나 웹브라우저에서 토큰이나 탈취되면 session보다 위험하다는 얘기는 반쪽짜리 이야기라는 것이다.

그럼 차이가 나는 부분은 어디냐.

앞서 말했던 stateful과 stateless에서 차이가 나는것이다. 즉 토큰 탈취시 서버에서 능동적으로 토큰을 expire할 수 있느냐 없느냐에 대한 부분이라고 볼 수 있다고 본다.

정리하면 특정 유저에 대한 디바이스별로 세세한 로그아웃 기능은 JWT 토큰만으로는 어렵다는 것이다.

image
(요런기능)

JWT 토큰만으로 사용자 식별을한다면 토큰 탈취 사고가 났을때는 할수 있는 방법은 백엔드에서 signature를 바꿔야하는 것이다. 그러면 기존에 인증을 하고 JWT 토큰을 발급 받은 모든 사용자는 잘 사용하다가 갑자기 튕기고 로그인 페이지가 뜰 것이다. 이건 또 사용자 사용성에서 별로인 정책이다.

즉 구글처럼 사용자별로 로그아웃 가능하게 해주어야할 것이다.

그럼 JWT 토큰을 쓰는 회사는 사용자별로 로그아웃 기능을 제공할 수 없나?

아니다 JWT이지만 jwt의 일부 정보 또는 전체를 서버에서 토큰 저장소를 운영해서 stateful하게 관리하면 특정 토큰을 expired 또는 denined 기능을 쉽게 구현할 수 있다.

그럼...

image
이이제 이런 표정이 나오만한게

stateless를 위해서 JWT를 썼는데 stateful하게 한다고?

그럼 session을 쓰지? 이렇게 이야기할 수 있다.

물론 이 글에서 전하고자하는 내용은

  1. JWT 토큰이 보안에 취약하다는 틀렸다(프론트/백엔드)
  2. session을 쓰는게 잘못됐다는게 아니다 기술을 바르게 보자는 내용이다.

session에 대비해서 JWT의 장점은 스타트업에서 빠른 개발을할 때이다.(물론 대기업도 일정 쪼이면 마찬가지)

과거의 한 회사에서는 browser용 API app용 API를 따로 제공했다.
왜냐하면 browser는 로그인이후 session을 이용했고 app은 로그인후 엑세스 토큰으로 통신을 하였기 때문이다.

이때 이걸 선택한 이유는 2가지로 생각이 된다.

  1. app은 cookiejar 기능이 없기 때문에 브라우저랑 다르게 동작해야한다.(사실 cookiejar 구현체를 쓰면 쉽긴한데...암튼...)
  2. app은 http 호출하여 json 형태로 응답하고 웹은 서버에서 http 호출시 html 형태로 응답하기 때문에 response content/type 미스매칭

사실 이때는 나도 이렇게 생각할 수 밖에 없었던거 같다
그래서 앱/웹 두벌을 만들기 인력/시간이 부족한 스타트업은 웹뷰를 이용한 앱을 많이 선택할 수 밖에 없었던게 당연한 흐름이였던거 같다.

하지만 몇년전 한회사에서 나는
사용자에게 제공할 앱은 flutter를 이용하여 ios/android를 개발하고
운영툴은 웹으로 vuejs로 개발을했었다.

나는 vuejs는 할 수 있으니 spring을 이용하여 api를 만들고 vuejs로 화면을 그리고 프론트와 백엔드간 api를 통해 데이터를 주고 받고 개발을 하였다.

이때 웹은 JWT를 이용하였는데

이때 느낀 신선함은 flutter 앱개발자에게도 동일한 api를 제공해도 된다는 점이였다.

즉 웹/앱 구별하여 api를 두벌 뽑을 필요가 없었다.

기술이 모던해지면서 vue나 reactjs와 같은 기술이 나왔고 이 Single Page Application(SPA)를 통해 web도 이제는 app과 비슷해졌고 이로 인해 백엔드 개발자는 좀 더 api 개발시에 효율을 느낄 수 있었다.

즉 나는 session 베이스는 response content type이 html일 때 자연스럽고
response type이 application/json일때는 JWT을 더 선호한다는 것이다.(무조건 이렇게 해야한다고 할 수 없지만 내 기준은 이렇다)

웹은 session 앱은 jwt를 쓴다면 api gateway에 시큐리티를 붙이고 블라블라 손을 대야하는데 못할건 없지만
의사 결정으로 jwt로 웹/앱 동시에 쓴다면 편리한면이 많아진다고 생각한다.

편리성 외에도 JWT 토큰을 사용해야하는 이유가 있는데 이는 요새 다들하는 MSA 아키텍쳐와 연관이 있다고 생각한다.

image

위의 이미지처럼 서버간 통신이 많아지는 상태에서 중앙화된 사용자 식별 저장소가 있다면 api 통신마다 거치면 서비스가 커지고 api 요청이 증가함에 따라 중앙화된 저장소에 가해지는 부하는 어마어마해질 수 밖에 없는 것이다. 우리가 대용량 트래픽을 견디기위해서는 디비의 부하를 줄이기위해 그 많은 노력을 가하듯이 사용자 식별 저장소의 부하를 견디기 위해 또 관리대상이 늘어나는 것이다.

그렇기에 interal api 통신은 100% 신뢰 관계라고한다면 그냥 api에 사용자 정보를 붙여서 api 사용하는 서버는 validation없이 진행해도 무방하겠지만

internal api간에도 https로 통신하는 마당에 보안에 신경을 안쓸 수가 있을까? 나는 internal api들도 최소 JWT를 통해 validate가 진행되어야한다고 보는 입장이다.


결론

그럼 내가 생각하는 결론은 무엇이냐

진리의 케바케이다

회사의 규모가 크고 또는 개발자의 개발 경험이 session 베이스 또는 이미 만들어진게 그렇다면
로마의 법에 따라야하고

신규 서비스이고 아키텍처를 새로 짜야한다면 session을 쓸 수도 있겠지만
나는 JWT를 이용한 설계를 더 선호할 것으로 보인다.

일단 internet facing한 api gateway는 jwt 토큰의 유효성 검증을 위해 토큰 관리 저장소(stateful)를 조회해서 토큰 유효성 검사를 할 것이고
내부 api간 통신에는 이 jwt 토큰을 그대로 bypass해서 signature 검증을 통해 api의 신뢰관계를 형성시키고자 할 것이다.


P.S I......

자꾸 JWT JWT하니까
image

위의 이미지처럼 JWK나 다른 애들도 얘기해보고 싶지만 내용이 너무 길어져서...(그리고 대충 아는 수준이라 좀 더 스터디가 필요하다.)

아무튼 조금 쓰면 JWK는 좀 재밌는게 jwt 토큰의 signature 생성을 위해 암호화 알고리즘과 키 변경에 어려움이 있어서 JWT에 대해 좀 이런면이 부족하다고 생각한게 있는데 이를 해결해주는 녀석이라 좋게 보고 있는 녀석임. 대충 써보긴했지만 프로덕션 적용 경험은 없어서 아쉽긴함.


주말엔 육아를해야하는데 회사일로 공부할게 있어서 겨우 허락받고 카페와서 컴퓨터를 켰는데 두시간째 딴짓중...
더 이상 글을 이어가면 죄책감이 들고.
내가 말이 많구나 싶음...

글 저장하고나면 리뷰는 없으니 오타 틀린 내용 많음 주의

http only cookie 라는 놈도 있습니다. 스크립트로는 접근할수 없어요.
전 만료외에는 헤더 방식이 더 유리한 점도 있다고 생각합니다. 물론 만료도 무상태에 국한된 문제이긴 하지만요.
쿠키 방식은 csrf 공격에 더욱 취약합니다.
그리고 쿠키 방식은 세션 고정 공격에도 역시나 취약합니다.
(물론 최근엔 잘 통하지는 않긴 하지만...)

감사합니다.

말씀을 듣고나니 브라우저 입장만 보면 쿠키 vs 헤더 내용중 document.cookie 논리는 보안상 더 유리하므로 틀린 내용이네요.

image
(혹시나 톰캣을 쓸것 같아서 okky의 쿠키를 살펴보니 JSESSIONID가 httponly라는 것을 알 수 있네요 뭐 당연히 session cookie는 프레임워크에서 당연히 설정하는게 당연하니 document.cookie는 틀린 논리요.)

사실 아래 이미지는 본문에 글 내용중 캡처한 이미지인데요

image
위에 이미지에 나온 쿠키들의 갯수와 document.cookie랑 다르다는건 글을 쓰면서 인지를 하긴했는데
뒤의 글을 이어가기 바빠서 제대로 정리를 못했네요 ㅠㅠ(틀린 내용에 대해 명확히 사실 여부를 잡을 수 있게해 주셔서 감사합니다 ㅎㅎ )

image

출처 MDN http cookies

(다른 분들을 위해 위의 설명이 잘되어 있는 내용을 첨부합니다.)


보안만 본다면 SameSite: Lax가 이제는 기본이 된 현재는
출처 : Cookies default to SameSite=Lax

쿠키가 예전보다는 좀 더 csrf에 대해서는 대응이 잘 될거라 보고 있긴하지만

크롬기준으로 75버전 이하의 레거시 브라우저는 여전히 취약하므로 말씀하신대로 보안적으로 쿠키가 취약할 수 있다는 유효한것 같습니다.

( 갑자기 SameSite: Lax 저걸 보니 꼭 한뻔씩 sub domain들 GET은 되고 POST가 안되는 제한때문에 삽질을 한 기억이... ㅠㅠ)


개발을 할 때 보안만 볼 수 없기에 여러 조건들에 대해서 트레이드오프를 봐야할텐데(그렇다고 Authorization 헤더를 쓰는게 보안에 취약하다는게 아니라...)

세션 Cookie를 쓸 때 겪게되는 여러가지 삽질의 시간들을 겪다보니 왠지 Authorization 헤더가 더 예뻐보이네요 ㅎㅎ;;

image

mdn SameSite cookie 이 링크를 보다가 위의 이미지를 봤는데요 아직 IE 지원도 안하는군요...

브라우저 서포트등 쿠키로 인한 복잡성을 피하려고 Authorization header를 쓰고 싶다는 충동이 들 수도 있겠다 싶네요;;

@gunlee01 항상 많이 배웁니다 감사합니다.

jwt 토큰은 어디에 저장하는가? 하는 부분이 같이 다뤄져야 보안성이라는 측면에서 균형감이 있겠어요.
아직까지는 http only and secure 쿠키가 html5 로컬 저장소들 보다는 안전에 대한 대비가 많이 되어 있기도 하고요.

@wafe 댓글 감사합니다 :) 저도 HttpOnly 부분을 제대로 짚지 못하고 넘어갔었네요. ㅎㅎ 위에 댓글에서 @gunlee01 님께서 관련해서 정리해주셨어 정리에 도움이 되었습니다 ㅎㅎ

그간 SameSite=None이 기본이여서 생기는 문제점이라고 생각하고 있어요.
그래서 브라우저는 LAX로 기본값을 바꿨는데 그래도 레거시 이슈도 있고...
다르게 생각해보면 모든 request에 쿠키가 자동으로 포함되면서 생기는 보안홀이였다고 보이는데요.

그래서 개발자가 ajax 호출시에 헤더에 명시적으로 넣어주는게 쿠키보다 안정적일 수 있다라고 보는 의견도 있는 것 같아요.

일단 이모든건 스크립트 인젝션이 가능한 상황에서 자바스크립트로 브라우저 보안에 대한 이슈로 볼 수 있을것 같은데요.
(일단 여기서부터 에러)

SameSite 이슈는 서버에서 CORS 관련으로 비슷하게 하고 있다고 봐서

HttpOnly Cookie 대비 local/session storage는 너무 쉽게 접근 가능하기 때문에 보안에 더 취약하다는 의견에는 이견이 없네요.

쿠키 방식은 csrf 공격에 더욱 취약합니다.
그리고 쿠키 방식은 세션 고정 공격에도 역시나 취약합니다.
(물론 최근엔 잘 통하지는 않긴 하지만...)

위의 내용처럼 쿠키는 쿠키 나름대로 약점이 있다 access_token은 나름의 약점이 있다로 봐야 수평이 맞을 것 같다는 생각을 갖고 있습니다 :)

생각해보니 기존에 사용한 방식대로 사고를해서 글을 전개했었는데요

http only & same site 세팅한 cookie 방식으로 jwt 토큰을 브라우저에 전달하는것도 방법이겠다는 생각이 드네요.

근데 또 제가 있는 서비스에서는 oauth라 여러 도메인들을 제어하는게 또 쉽지 않은 문제네요.

자신이 속한 서비스의 특성에 맞게 잘 설계를하는게 핵심인 것 같습니다.

저는 여러가지를 고려했을때 아직도 jwt bearer header 방식을 선호하는 것 같네요

commented

글과 댓글 전부 잘 보았습니다. 좋은 게시글 감사합니다!