안전한 로그인 토큰 관리, 프론트엔드의 필수 전략
2024년 11월 22일
서문
현대 기술 환경에서 사용자 인증 방식은 날로 변화하고 있으며, 그에 맞춰 보안 방식도 끊임없이 발전하고 있습니다. 높은 확장성과 유연성을 지닌 토큰 기반 인증 방식은 빠르게 표준화되고 있으며, 특히 보안성과 효율성을 이유로 전통적인 세션 기반 인증 방식을 대체하는 사례가 증가하고 있습니다. 쿼리파이 또한 보안성을 높이기 위해 제품에 토큰 기반 인증 방식을 도입했습니다.
프론트엔드 진영에서의 새로운 도전
웹 프론트엔드 진영에서 인증을 구현한 역사는 그렇게 길지 않습니다. 전통적인 세션기반의 인증방식은 서버가 사용자 세션을 관리합니다. 프론트엔드 진영에서 인증이란 Form 을 잘 만들어서, 서버가 지정한 엔드포인트에 id와 password를 잘 전송하는 것으로 끝나는 역할이었습니다. 그러나 최근 몇년간 프론트엔드 진영은 여러가지 기술적인 변화를 맞이하게 됩니다.
AJAX 의 등장과 Single Page Application 구조 덕분에 프론트엔드 개발자들은 백엔드와 JSON 기반의 API통신으로 데이터를 주고 받게 됩니다.
Web Storage API 가 등장하며 프론트엔드에서 영속성 (persistence)를 쉽게 처리하게 되었습니다.
NextJS와 같은 SSR 서버의 등장으로 이전에 서버에서 하던 역할들이 프론트엔드로 넘어오게 됩니다.
위의 변화는 토큰 기반의 인증 방식과 맞물려 프론트엔드 진영에 안전한 토큰 송수신 및 영속성 관리라는 새로운 과제를 던져주게 되었습니다.
안전한 토큰, 그렇지 못한 사용자 환경
토큰 기반 인증은 개발 편의성 뿐 아니라 토큰의 위변조가 불가능에 가깝기에 보안적인 측면에서도 우수합니다. 하지만 토큰 자체가 탈취된다면, 이야기가 달라집니다.
위협의 종류
토큰을 탈취할 수 있는 주요 경로는 사용자 브라우저입니다. 사용자가 오래된 브라우저를 사용한다면, 브라우저 자체의 취약점을 공략당할 수 있습니다. 근래에는 크롬과 같이 언제나 사용자가 최신버전을 사용하게 하는 에버그린 브라우저의 점유율이 높기 때문에, 브라우저의 취약점은 빠르게 수정이 됩니다.
오히려 취약한 부분은 브라우저가 제공하는 여러가지 보안 수준을 만족하지 않는 자바스크립트 코드나, 서버 설정일 가능성이 더 큽니다. 이러한 상황을 악용하여 공격자는 Cross Site Scripting (XSS), Cross Site Request Forgery (CSRF), Session Hijacking 공격으로 토큰 탈취, 또는 그에 준하는 공격을 할 수 있습니다.
Cross Site Scripting (XSS)
https://owasp.org/www-community/attacks/xss/
Cross-Site Scripting (XSS) attacks are a type of injection, in which malicious scripts are injected into otherwise benign and trusted websites. XSS attacks occur when an attacker uses a web application to send malicious code, generally in the form of a browser side script, to a different end user. Flaws that allow these attacks to succeed are quite widespread and occur anywhere a web application uses input from a user within the output it generates without validating or encoding it.
XSS 공격이란 사용자 브라우저에 악의적인 스크립트를 구동시키는 방식이며, 전통적인 code injection 공격입니다. 여러가지 공격방식이 있지만 가장 흔한 방식의 예는 아래와 같습니다.
공격자는
vulnerable-site.com/search?query=${query}의 스킴으로 접속했을 때, query search parameter가 그대로 결과 페이지에 출력되는 취약점을 확인 합니다.공격자는
vulnerable-site.com/search?query=<script>...악성코드...</script>를 넣었을 때 결과 페이지에서 악성코드가 전송되는 링크를 만듭니다.이후 공격자는 메신저나 커뮤니티등을 이용하여 악성 링크로 다른사용자들이 들어가도록 링크를 만듭니다.
만약 해당 사이트 이용자의 access token이 js를 통해 획득할 수 있는 상태라면 탈취 위협에 노출됩니다.
Cross Site Request Forgery (CSRF)
https://owasp.org/www-community/attacks/csrf
Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated. With a little help of social engineering (such as sending a link via email or chat), an attacker may trick the users of a web application into executing actions of the attacker’s choosing. If the victim is a normal user, a successful CSRF attack can force the user to perform state changing requests like transferring funds, changing their email address, and so forth. If the victim is an administrative account, CSRF can compromise the entire web application.
CSRF 공격은 그자체로 토큰을 탈취하는 위협은 아닙니다. 그러나 XSS 공격과 마찬가지로 공격자는 웹 사이트의 설계 헛점과 사용자 인증상태를 이용하여, 사용자에게 피해를 줄 수 있는 행위를 시킬 수 있습니다. 이는 토큰 탈취 만큼 중대한 취약점입니다.
공격자는 사용자에게 피싱 메일을 보내서
vulnerable-shop.com사이트인척scam-shop.com링크를 누르도록 속입니다.vulnerable-shop.com에 로그인되어있던 사용자는 해당 링크를 누릅니다.scam-shop.com페이지에는 아래와 같은 스크립트가 페이지 로드시 자동으로 실행됩니다.
<form id="form" action="https://vulerable-shop.com/api/purchase" method="POST"> <input type="hidden" name="item_id" value="$expensive_item"> <input type="hidden" name="address" value="$attackes_house"> <input type="hidden" name="amount" value="10000"> <button type="submit">Purchase</button></form><script> document.getElementById('form').submit();</script>사용자가 이미 로그인 되어있고, 적절한 보안 설정이 되어 있지 않다면, 사용자는 본인도 모르는 사이에 공격자에게 비싼 물건을 10000개나 결제해서 보내게 됩니다.
CSRF는 cookie의 성격을 이용한 공격인데, cookie는 아래와 같이 동작합니다.
Cookie는 사용자 브라우저에 저장됩니다.
Cookie는 해당 cookie에 설정된 도메인으로 요청시 언제나 포함됩니다.
프론트엔드 인증 보안: 쿼리파이의 RED 팀과 함께하는 모범 사례 및 위협 진단
인증을 잘 구현하기는 매우 어렵습니다. 특히 일반적인 기능 개발뿐만 아니라 보안도 잘 챙겨야합니다. 쿼리파이라는 보안 솔루션을 만들면서, 프론트엔드 팀도 보안 솔루션에 인증을 제대로 붙이기 위하여 많은 고민을 하고 리서치를 했습니다. 인증을 구현하는 과정에서 쿼리파이의 RED 팀도 중요한 역할을 했습니다. RED 팀은 자사 프로덕트를 대상으로 화이트 해킹을 통해 잠재적인 취약점을 발견하고, 이를 개선하는 데 큰 기여를 했습니다. 덕분에 더욱 안전한 인증 체계를 구축할 수 있었습니다.
문서 초기에 언급한대로, 프론트엔드에서 인증처리란 아직은 그리 성숙한 분야가 아닙니다. 인터넷에 있는 인증을 구현하는 방법에 대한 여러 가이드는 실제로 보안적으로 취약한 것들이 많기 때문에, 정확한 정보를 골라내기 쉽지 않습니다.
쿼리파이에서 실제로 프론트엔드 개발자가 작성하는 코드에 어떤 위협이 있는지 예시를 통하여 프론트엔드 코드에서의 위협을 진단하고, 모범 사례를 제안드리겠습니다. 이 글을 보고 나면 더이상 프론트엔드에서의 인증 처리는 고민이 필요 없을 겁니다.
예시 - 초보 프론트엔드 개발자의 SPA 인증 구현
아래는 Single Page Application에서 흔히 보이는 로그인과 인증이 필요한 api 요청 코드입니다.
async function login(id, pw) { const res = await fetch('<cross_origin_api_url>/api/auth', { method: 'POST', body: encrypt({id, pw}), mode: 'cors' }); const token = await res.json(); localStorage.setItem('accessToken', token.accessToken);} async function getProtectedResource(id) { const accessToken = localStorage.getItem('accessToken'); const res = fetch(`<cross_origin_api_url>/api/protected-resource/${id}`, { headers: { Authorization: `Bearer ${accessToken}` }, mode: 'cors' });}해당 코드는 아래와 같은 히스토리가 있습니다.
Backend 와의 API 규약
Authorization헤더에 token을 포함한다.Access-Control-Allow-Origin헤더는 개발 편의를 위해*로 설정한다.