
그동안 CSRF(Cross-Site Request Forgery) 라는 개념을 알고는 있었지만, 왜 필요한지, 그리고 어떻게 사용하는지에 대해 제대로 이해하지 못한 상태였다. 이번 프로젝트를 계기로 CSRF의 정의와 그 중요성을 명확히 이해하게 되었고, SSR과 CSR 환경에서의 처리 방식 차이도 알게 되었다.
CSRF는 사용자를 속여 의도하지 않은 요청을 특정 서버로 보내는 공격 이다.
사용자가 A 웹사이트에 로그인한 상태에서 공격자가 조작한 링크를 클릭할 경우, 브라우저는 자동으로 A 서버에 저장된 세션 쿠키를 포함해 요청을 보낸다.
만약, A 서버가 CSRF 방어를 하지 않는다면 공격자가 사용자의 세션을 악용해 계좌 이체, 비밀번호 변경 같은 민감한 작업을 수행할 수 있다.
CSRF가 가능한 이유는 다음과 같다:
자동 세션 관리: 브라우저는 동일한 도메인으로 요청을 보낼 때 세션 쿠키를 자동으로 포함한다.
요청의 출처를 확인하지 않음: 서버가 요청이 사용자 의도인지 검증하지 않고 세션 쿠키만으로 요청을 처리한다.
CSRF를 방어하기 위해 서버는 요청의 정당성을 검증하는 메커니즘을 사용한다.
CSRF 토큰: 서버는 클라이언트에게 고유 토큰을 제공하고, 요청마다 이를 확인해 요청 출처를 검증한다.
사용자가 조작된 링크를 클릭한 경우, CSRF 토큰이 없거나 불일치해 요청이 차단된다.
Referrer/Origin 검증: 서버는 요청 헤더를 확인해 신뢰할 수 있는 도메인에서 발생한 요청인지 확인한다.
특히, CSRF 방어는 서버 상태를 변경하는 요청(예: POST, PUT, DELETE)에서 필수적이며, GET 요청에는 적용되지 않는다.GET 요청은 데이터를 가져오는 데 사용되며, 일반적으로 서버의 상태를 변경하지 않기 때문이다.
프로젝트를 진행하며 SSR(Server-Side Rendering)과 CSR(Client-Side Rendering)에서 CSRF 처리 방식이 다르다는 점을 알게 되었다.
SSR 환경에서는 클라이언트 - 프론트 서버 간의 요청에서 CSRF 방어가 필요하다.
예를 들어, next-auth 는 CSRF 토큰을 자동으로 생성하여 클라이언트가 프론트 서버에 요청을 보낼 때 이를 검증한다. 이렇게 함으로써 프론트 서버는 요청이 의도된 클라이언트에서 발생했음을 확인할 수 있다.
하지만 프론트 서버 - 외부 서버 간 통신에서는 CSRF 방어가 필요하지 않다.
서버 간 통신은 클라이언트 세션을 악용할 수 없기 때문이다. 따라서, SSR 환경에서 프론트 서버와 외부 서버가 통신만 한다면 외부 서버는 CSRF 토큰을 발급하지 않아도 된다.
SSR을 할 때, 백엔드 개발자가 CSRF Token을 발급해준다고 하면(프론트 서버로만 외부 서버로 통신을 한다는 가정하에... 즉 서버 리스 함수로만 외부 서버와 통신하는 경우 말이다), 필요하지 않다고 말하자! 그렇지 않으면 나처럼 삽질을 하게 된다🙃🙃
CSR 환경에서는 프론트 서버가 없기 때문에 클라이언트 - 외부 서버 간 요청에서 CSRF 방어가 필수적이다.
외부 서버는 사용자가 로그인할 때 CSRF 토큰을 발급하여 클라이언트에게 전달(응답 헤더 또는 쿠키)한다. 이후 클라이언트는 이 CSRF 토큰을 요청 헤더에 포함하여 외부 서버에 보낸다.
외부 서버는 요청을 처리하기 전에 CSRF 토큰의 유효성을 검증하여 요청이 신뢰할 수 있는 클라이언트에서 발생했음을 확인한다.
SSR: 클라이언트 - 프론트 서버 간 CSRF 방어가 필요하며, 프론트 서버와 외부 서버 간 통신에서는 CSRF 방어가 불필요하다.
CSR: 클라이언트 - 외부 서버 간 CSRF 방어가 필요하며, CSRF 토큰을 발급하고 이를 GET 요청을 제외한 모든 요청마다 검증해야 한다.
그동안은 보안과 관련하여 프론트 개발자인 내가 신경써야할 것이 별로 없다고 생각했었다. 그러다 보니 삽질도 많이 하게 되었다. 하지만 이제는 프로젝트의 아키텍처와 배포 방식에 따라 적합한 CSRF 방어 전략을 선택할 수 있는 자신감이 생겼다. 앞으로도 보안 요소를 더욱 깊이 고민하며 안정적인 서비스를 설계해 나가야 겠다.