실제로 운영할 서버를 만들면서, 보안에 대해서도 신경을 굉장히 많이 쓰게 되었다.
XSS 공격과 CSRF 공격을 막기 위해 공부한 것들을 정리하겠다.
우선 세션 로그인 방식과 jwt 토큰 발급 방식에 대해 비교해 보도록 하겠다.
세션 로그인 방식과 jwt 토큰을 활용한 로그인 방식
두 가지 중 우리는 프로젝트에 어떤 로그인 방식을 도입해야 할까?
우선 세션 로그인 방식이 뭔지부터 알아보도록 하자
세션 로그인 방식
사용자가 로그인을 시도한다.
데이터베이스에서 인증 정보를 확인한다.
세션 id를 발급하고 해당 정보를 세션 저장소에 저장한다.
세션 id를 쿠키로 설정해서 프론트엔드에 반환한다.
세션 id가 쿠키에 설정된 이후 플로우
사용자가 요청을 보낸다.
쿠키에 세션 id 정보가 포함돼 있다.
서버의 세션 저장소에서 세션 id를 찾고 세션 id가 존재한다면, 들어온 요청을 수행해서 반환한다.
대략적으로 이러한 흐름이다.
이제 장단점을 살펴보자
세션 로그인 방식의 장점
사용자의 인증 정보를 모두 서버에서 관리하기에 보안에 용이하다.
쿠키의 만료기간이 끝날 때까지 사용자는 로그인을 다시 하지 않아도 된다.
http only와 secure 등의 옵션을 추가하면 XSS 공격으로부터 안전하다.
세션 로그인 방식의 단점
다중 서버 환경에서 세션 정보의 불일치 문제가 발생할 수 있다. (통합 인증 문제)
쿠키에 인증 정보를 저장하기 때문에 CSRF 공격에 취약하다.
사용자가 많아질 수록 세션 저장소의 크기가 커진다.
이러한 장단점이 존재한다.
그럼 이러한 단점들은 어떻게 극복할 수 있을까?
세션 로그인의 단점을 극복해 보도록 하자
먼저 첫 번째 다중 서버 환경에서 세션 정보의 불일치 문제 해결 방법
Sticky Session
한 번 요청이 들어간 서버에 계속해서 요청을 넣는 방식이다.
한 서버에 부하가 몰릴 수도 있다는 단점이 존재한다.
별도의 세션 저장소 설정
redis나 memcached 같은 캐시를 사용해서 별도의 세션 저장소에 모든 세션 정보를 저장하는 방법이다.
CSRF 공격 대응법
서버에서 CSRF 토큰을 발급하고 이를 사용하면 된다.
이러한 대응법이 존재한다. 그러나 세션 저장소의 크기가 커지는 것은 마땅한 대응책이 존재하지 않는다.
그래서 나타난 것이 바로 JWT 토큰이다.
jwt 토큰은 클라이언트 측에 인증 정보를 저장하기 때문에, 별도의 세션 저장소를 만들 필요가 없다.
이 말은 확장성에 유리하다는 의미이고, 최근 유행하는 MSA 아키텍쳐에서 뛰어난 강점을 보인다.
MSA 아키텍쳐가 유행하며, JWT 토큰의 로그인 방식이 급 부상하기 시작했다.
로그인 흐름부터 알아보도록 하자
JWT 토큰 로그인 방식
사용자가 로그인을 시도한다.
데이터베이스에서 인증 정보를 확인한다.
액세스 토큰과 리프레시 토큰을 발급하여 반환한다.
로그인 성공 이후 플로우
인증이 필요한 모든 요청 헤더에 액세스 토큰을 추가한다.
액세스 토큰을 복호화하며 검증을 시도한다.
검증에 통과했다면, 해당 요청의 반환 값을 반환한다.
토큰이 만료된 경우
액세스 토큰의 만료 시간이 지나 토큰이 만료됐다.
검증에 실패하고, 401 에러가 반환된다.
프론트에선 해당 응답을 받고 사용자가 눈치채지 못하게 리프레시 토큰을 활용하여 토큰을 재발급한다.
이후 재발급 받은 액세스 토큰을 활용하여 다시 요청을 보낸다.
이러한 흐름이다.
이제 장단점을 살펴보도록 하자.
JWT 토큰 로그인 방식의 장점
앞에서 말했듯이 별도의 인증 정보 저장소가 필요하지 않다.
인증 정보를 클라이언트 측에서 관리하니 확장성에 유리하다.
JWT 토큰 로그인 방식의 단점
XSS 공격과 CSRF 공격에 대비하기 위해 준비해야 할 것이 많다. (보안에 상대적으로 취약함)
JWT 토큰은 base64로 인코딩 되었기에, 누구나 디코드가 가능하다. 즉, 토큰에 민감한 정보를 담으면 안 된다.
리프레시 토큰을 저장하기 위한 별도의 저장소가 필요하다.
(리프레시 토큰을 저장하는 것에서 세션 로그인 방식과 도긴개긴인 거 같은 느낌이 들기도 한다.)
jwt 토큰을 사용할 때 XSS 공격과 CSRF 공격을 막기 위해 어떤 조치를 취해야 하는지는 다음 포스트에 정리하도록 하겠다.
결론
자 로그인 방식의 장단점에 대해 살펴 보았다. 우리는 어떤 로그인 방식을 선택할 것인가.
우리 프로젝트의 특징을 살펴보자, 우선 ASG를 적용하여 동적으로 서버의 개수가 변하도록 만들 것이다.
추후, 지원자 평가 등의 기능을 추가하며 더 확장을 시킬 가능성이 존재한다.
따라서 우리는 별도의 저장소가 필요하지 않고 확장에 용이한 JWT 토큰 로그인 방식을 채택하도록 하겠다.
지난 포스트에서 로그인 방식을 선정해보았다.
이제 선정한 jwt 토큰 기반 로그인 방식을 사용할 때 어떤 보안 조치를 해야하는지 살펴보도록 하겠다.
우선 XSS 공격이란 무엇일까?
XSS 공격
Cross Site Scripting의 약자로 게시판이나 웹 메일 등에 자바 스크립트와 같은 스크립트 코드를 삽입해
전혀 다른 기능이 동작하게 하는 공격이다.
이런 방식으로 동작하며, 사용자가 글을 입력할 수 있는 게시판, 웹 메일 등에서 XSS 공격이 유효하다.
그럼 XSS 공격을 통해 어떤 피해가 발생할 수 있을까
사용자의 쿠키 및 민감한 정보 탈취
사용자의 자바스크립트에 접근해 쿠키 정보 및 여러 정보를 탈취한다.
이를 이용해 서버에, 쿠키에 담긴 인증 정보를 보내 여러 작업을 수행할 수 있다.
악성 코드 다운로드
악성 스크립트 안에 URL을 포함시켜, 악성 코드를 다운받는 사이트로 보낸다.
이제 해결방안을 살펴보도록 하자.
사용자의 쿠키 정보 탈취 대응 방안
사용자의 쿠키 정보 탈취에 대한 대응 방안은 바로 HttpOnly 옵션을 설정해두는 것이다.
해당 옵션은 자바스크립트를 통한 쿠키 접근을 막는 옵션이다.
악성 코드 다운로드 대응 방안
사용자가 입력하는 모든 텍스트에 정규식을 적용하여 필터링을 수행하면 된다.
<>과 같은 특수문자의 입력을 제한하면 해결할 수 있다.
그러면 HttpOnly 옵션으로 쿠키를 설정하면 쿠키에 저장된 정보를 빼앗길 일이 없나요? 라고 생각할 수 있다.
하지만 중간자 공격이라는 변수가 존재한다.
중간자 공격
중간자 공격은 단순하게 네트워크 상 전송되는 데이터 패킷에서 쿠키 정보를 탈취하는 것이다.
네트워크 전송 간 암호화 되지 않은 Http 프로토콜을 사용한다면 쿠키 정보를 아주 간단하게 빼앗길 수 있다.
반대로 말하면 Https 프로토콜을 통해 암호화하여 데이터를 전송한다면 쿠키 탈취를 막을 수 있다는 얘기다.
Https에서만 쿠키 전송을 허용한다는 "Secure" 옵션을 설정해주면 된다.
중간 정리
자 그러면 현재까지의 내용을 정리하면 "쿠키에 HttpOnly 옵션과 Secure 옵션을 설정하여
인증 정보를 모두 쿠키에 저장하면 되겠네요!" 라는 결론이 나온다.
그러나 인증 정보를 모두 쿠키에 저장하는 방식을 사용한다면 CSRF 공격에 취약해질 수 있다 ㅠㅠ
CSRF 공격이 뭔데요??!
이제부터 알아보도록 하자
CSRF 공격
현재 쿠키에 모든 인증 정보가 저장된 상황이다.
넷플릭스 시청을 위해 로그인을 한다.
로그인을 할 때 모든 인증 정보가 쿠키에 저장된다.
다음에 넷플릭스에 요청을 보낼 때 쿠키에 설정된 인증 정보가 같이 자동으로 전송되기에, 별 다른 인증 절차가 필요하지 않아진다.
악성 스크립트가 심어진 evil.com에 접속했다.
여러 가지 유혹광고를 뿌리치지 못 하고 한 가지 광고를 클릭했다.
이런! 해당 광고엔 다음과 같이 넷플릭스에 아이디와 비밀번호 변경을 요청하는 post 요청이 숨어있었다.
당신의 넷플릭스 아이디와 비밀번호가 바뀌게 되었다 ㅠㅠ
대략 이러한 흐름을 지니고 있다.
자 이제 인증 정보를 쿠키에 저장하면 안 된다는 사실을 알았을 것이다.
그렇다면 해결 방법은 무엇일까?
세션 ID를 사용하는 경우엔 CSRF 토큰을 사용하면 된다.
하지만 우리는 JWT 토큰을 사용 중이다.
JWT 토큰 사용 시 XSS 공격과 CSRF 공격을 방어하는 법
액세스 토큰을 이용한다면 바로 인증 없이 모든 요청을 보낼 수 있다는 사실을 알고 있을 것이다.
그렇다면 액세스 토큰을 쿠키에 저장하지 않고 다른 저장소에 저장하면 된다.
그 위치는 바로 자바스크립트의 private 밸류이다.
나는 프론트에 대해서 잘 알지 못 하지만 나름대로 열심히 찾아보았다.
자바스크립트에서 private 밸류로 변수를 설정 해두면 외부에서 접근할 방법이 없다고 한다.
자바스트립트의 private 밸류는 메모리에 변수를 저장하는 방식으로 메모리에 변수 값을 저장하니 당연히
자바스크립트를 통한 접근이 불가능하고, 이는 XSS 공격을 막는 수단이 될 수 있다.
여기서 단점이 하나 발생한다. 액세스 토큰을 메모리에 저장한다면 보안상으로는 훌륭하지만,
사용자가 새로고침을 할 때마다 메모리의 값이 날라가기 때문에 다시 액세스 토큰을 재발급 받아야한다는 점이다.
리프레시 토큰을 쿠키에 설정해두고, 사용자의 액세스 토큰이 만료되거나, 새로고침을 했을 경우 자연스럽게 토큰을 재발급 받을 수 있도록
코드를 작성하는 불편함만 조금 감수한다면, 우리는 훌륭한 보안 매커니즘을 가져갈 수 있다.
그렇기에 액세스 토큰을 재발급 받을 수 있는 리프레시 토큰만 쿠키에 설정해두고 사용하는 것이다.
여기서 의문이 하나 더 생길 수 있다.
리프레시 토큰을 사용해서 액세스 토큰을 재발급 받는 CSRF 공격이 있다면 어떡하죠?
맞다. 그래서 리프레시 토큰 하나만을 사용해서 액세스 토큰을 재발급 받을 수 없도록 로직을 만들어야 한다.
또한 리프레시 토큰을 액세스 토큰처럼 사용하지 못 하도록 서버에서 리프레시 토큰 발급 과정에 액세스 토큰 발급 과정과 차별점을 두어야 한다.
글이 길어진다 결론을 내보자
결론
액세스 토큰은 메모리에 저장하여 외부 접근을 차단한다.
리프레시 토큰은 쿠키에 HttpOnly, Secure 옵션을 설정하여 저장한다.
새로고침하거나 만료 시간이 지나 액세스 토큰이 사라진다면, 사용자 모르게 리프레시 토큰을 사용해서 액세스 토큰을 재발급 받아 사용해라
리프레시 토큰 단 하나만을 이용해서 토큰을 재발급 받을 수 없게 만들어라
나의 경우엔 사용자의 아이디나 이메일 같은 개인정보와 리프레시 토큰을 매핑 시켜, 해당 정보가 일치한다면 토큰을 재발급 받을 수 있도록 해두었다.
당연히 액세스 토큰과 리프레시 토큰은 발급 과정에 차이를 두어 리프레시 토큰 값을 이용해서 엔드 포인트에 접근할 수 없도록 해야한다.
이렇게 정리를 할 수가 있겠다.
여러 글을 찾아보는데, 로컬 스토리지에 액세스 토큰을 저장하라는 말이 너무나 많았다.
그러나 로컬 스토리지에 액세스 토큰을 저장하면 너무 쉽게 토큰을 탈취 당할 수 있으니, 별 다른 이유가 없다면
메모리에 액세스 토큰을 저장해서 사용하도록 하자
사용자 경험 개선을 위한 사용자 몰래 토큰 재발급하기 기능도 추가하는 것을 잊지 말고 ...!
참고한 사이트
https://2junbeom.tistory.com/142
https://2junbeom.tistory.com/143