JWT를 조금 더 안전하게 저장하기 & 쿠키와 웹 스토리지

Ryu Hyun Seung·2022년 4월 20일
7
post-thumbnail

동기

  • 우아한테크코스 Level 1, 자바스크립트로 자판기 SPA 구현하기 과정 중 회원 시스템의 인증/인가 요구사항 중 JWT 저장 위치에 따른 보안 이슈가 생겨 이를 해결하기 위해 조사 시작.

JWT란?

JSON Web Token의 약자로 모바일이나 웹에서 클라이언트와 서버 간 통신 시 사용자의 인증을 위해 사용하는 암호화된 토큰을 의미한다.
JWT 정보는 주로 통신 시 헤더의 Authorization에 담아 전송되며 이것을 서버에서 검증하여 사용자의 정보 열람, 권한 부여 등의 인증/인가 작업을 수행할 수 있다.

JWT의 구조

  • Header: 위 3가지 정보를 암호화할 방식과 타입이 들어간다.
  • Payload: 서버에서 보낼 데이터가 담긴다. 일반적으로 유저의 고유 id값, 유효기간이 들어간다.
  • Signature: Base64 방식으로 인코딩한 Header, payload 그리고 SECRET KEY를 더한 후 서명된다.

탈취와 공격 방법

인증/인가에 활용되는 JWT이 탈취된다면, 탈취된 사용자의 개인정보를 조회하거나, 인가되지 않은 범위의 접근 등에 악용될 수 있어 보안에 중대한 위험을 끼치게 된다.
주요 탈취 경로는 대표적으로 XSS 공격CSRF 공격이 있다.

키워드 설명

  • XSS 공격 :
    공격자가 상대방의 브라우저에 스크립트가 실행되도록 하여 사용자의 세션 혹은 웹사이트를 변조하여 악의적인 콘텐츠/피싱 공격을 말한다.

예시 :
게시글 내용에 script 태그를 입력하여, 게시글을 조회 시 허용되지 않은 스크립트를 실행하는 행위.
→ 주로 백엔드 측에서 게시글 정보를 데이터 베이스에 저장할 때 적절히 필터를 하지 않을 시 XSS 공격이 발생.


  • CSRF 공격 :
    사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위를 특정 웹사이트에 요청하게 하는 행위.

예시 :
https://compy.life/logout으로 GET 요청을 보내었을 때
계정을 로그아웃 처리한다면, 아래와 같이 img 태그로 사용자의 브라우저에 노출만 된다면?

<img src="https://compy.life/logout">

사용자가 의도하지 않았지만, 사용자의 계정은 로그아웃 될 것이다.
이외에도 피싱 사이트를 통해 form 태그의 method 속성을 POST로 설정하여 특정 웹사이트로 POST를 보내어준다면?


그렇다면 "안전한" 저장 위치는?

저장 위치는 크게 비공개 변수, 로컬 스토리지, 세션 스토리지, 쿠키로 나뉘어진다.

비공개 변수

다른 방식에 비해 보안에서 가장 안전하지만 페이지를 이동하거나, 새로고침만 하여도 토큰 정보가 휘발되어
사용자 경험에 좋지 않아 사실상 단독적으로 사용이 불가능하다.

세션 스토리지

페이지를 새로고침하거나, 이동하여도 토큰이 유지되지만
새로운 탭에서 접속 시 세션이 나뉘어지고, 브라우저가 종료되는 순간 휘발되어 세션 스토리지도 사용자 경험에 좋지 않다.

세션 스토리지는 모든 자바스크립트 코드를 통해 액세스 할 수 있음으로 XSS 공격에도 취약하나
자바스크립트 코드로 제어가 필요하기에 CSRF 공격에서는 안전하다.

로컬 스토리지

로컬 스토리지는 페이지를 이동하거나, 브라우저를 다시 시작하여도 만료 없이 유지된다.
하지만, 세션 스토리지와 동일하게 모든 자바스크립트 코드를 통해 액세스 할 수 있음으로 XSS 공격에 취약하고 CSRF 공격에 안전하다.

쿠키

옵션 없이 기본적으로 작동되는 쿠키는 사실 XSS, CSRF 공격에 모두 취약하다!
하지만, 백엔드와의 협업을 통해 쿠키에 httpOnly / secure / SameSite 옵션을 사용한다면

쿠키는 자바스크립트 코드 상에서 접근이 불가능해지고, HTTP 요청에만 포함되어 보내진다.
httpOnlysecure, SameSite 등의 옵션은 백엔드 측에서 설정이 필요하며, 브라우저와 통신 시 아래와 같이 서버 측 응답 헤더를 통해 지정된다.

Set-Cookie: 쿠키명=쿠키값; path=/; secure; SameSite=Lax

본격적으로 JWT 핸들링하기

결국 안전한 JWT 저장을 위해서는 백엔드와 프론트엔드의 협업이 필요하고, 서로 간의 이해가 필요하다.

시작 전 키워드 설명

  • Access Token: 권한이 필요한 요청 시 사용되는 토큰, 유효 기간을 짧게 한다 (시간 단위)
  • Refresh Token: Access Token이 만료되었을 때 갱신할 수 있는 토큰, 유효 기간은 길게 설정한다 (보통 일단위, 로그인 유지 기간)
  • 쿠키 옵션 :
    httpOnly → 스크립트 상에서 접근이 불가능하도록 한다.
    secure → 패킷 감청을 막기 위해 https 통신 시에만 해당 쿠키를 사용하도록 한다.
    SameSiteStrict, Lax 모드가 있으며 Strict 모드에서는 같은 도메인 범위에서만 해당 쿠키를 사용하게 하며, Lax는 사용자가 페이지 이동 시 혹은 Form을 통한 Get 요청 시에만 허용된다.

JWT를 더 안전하게 저장하기

  1. 백엔드 → 로그인 시 인증 서버로부터 access token, refresh token을 받아온다.
  2. 프론트엔드 → access token은 메모리(변수)에 저장한다.
  3. 백엔드 → refresh token은 쿠키에 저장하여 httpOnly / secure / SameSite(Strict or Lax 모드) 옵션을 지정한다. (백엔드)
  4. 프론트엔드 → 권한이 필요한 요청 시 Authorization 헤더에 access token을 보내준다.
  5. 백엔드 & 프론트엔드 → access token이 만료되었거나, 페이지 이동으로 사라졌을 시, 서버 렌더링 과정 혹은 API 통신을 통해 재발급을 요청한다.
    → 이때, 요청 시 쿠키에는 자바스크립트에서 접근이 불가능한(httpOnly 옵션) refresh token이 이미 담겨진 상태로 서버와 통신하게 된다.
  6. 백엔드 → refresh token이 만료되었을 때 DB와 다시 한번 통신하여 갱신 혹은 로그아웃 상태로 렌더링을 하여준다.

위와 같이 진행하게 된다면?

  • refresh token이 저장된 쿠키는 외부 경로와 자바스크립트 상에서의 접근이 불가능하여 CSRF, XSS 공격에서 모두 안전성이 확보된다.
  • access token이 저장된 비공개 변수는 XSS, CSRF 공격을 시도할 방법이 사라지며, 토큰이 휘발되어 사라졌던 UX 문제도 해결된다.

틀린 내용, 피드백이 있으시다면 댓글 남겨주시면 감사하겠습니다! 🙇‍♂️

profile
날 것에서 우아해지기 위한. 코딩하고, 배우고, 쓰기

3개의 댓글

comment-user-thumbnail
2022년 8월 30일

훌륭한 방식이군요!

답글 달기
comment-user-thumbnail
2023년 1월 26일

좋은 글 감사합니다. 질문이 있어 남깁니다.
access token을 메모리에 담고 페이지 이동시 토큰이 사라져버리면 다시 token 요청을 하게 된다고 했는데,
refresh token을 이용해서 access token을 다시 발급받게 될텐데 그럼 페이지 이동시 마다 데이터베이스에서 refresh token을 확인하는 과정이 있는건가요? 그렇게 된다면 session 방식과 차이가 없어보이는데 제가 잘못 이해한걸까요?

1개의 답글