CSRF 공격 방지 - SameSite 쿠키, CSRF 토큰
인증과 관련하여 Spring Security에 대해 학습하던 중 CSRF라는 키워드에 갑자기 궁금증이 생겼다.
최근에는 대부분의 백엔드 서버가 REST API 형식으로 데이터를 제공하여 CSRF에 대한 검증을 하지 않는다 하였다.

그렇다면 기존의 세션 방식으로 인증을 진행할 때는 왜 CSRF 공격에 대한 대비를 해야했을까??
CSRF 공격이란?
Cross-Site Request Forgery의 줄임말로 사이트 간 요청 위조라는 의미이며,
공격자가 인증된 사용자의 권한을 악용하여 사용자가 의도하지 않은 요청을 다른 사이트로 보내도록 하는 공격 기법이다.
CSRF 공격 테스트
우선 테스트를 위해 공격 대상이 될 게시판 서비스를 생성하였다.

그리고 CSRF 공격을 시도하기 위해 피싱 페이지를 제작하였다.

이 둘은 서로 다른 서비스임을 가정하기 위해 게시판 서비스는 localhost로 접근, 피싱 사이트는 127.0.0.1로 접근하였다.
이제 게시판 서비스에 로그인한 사용자가 피싱 사이트에 접근할 경우 사용자의 인증 정보를 활용하여 공격자가 의도한대로 게시글이 등록되게 할 것이다.
이 때 CSRF 공격을 두가지 방법으로 구현하였다.
1. form을 활용한 CSRF 공격
페이지 로딩 시 자동으로 csrfForm을 submit 하도록 하여 사용자의 인증 정보를 활용하여 게시글을 작성하는 방식이다.
<!-- 방법 1: Form & iframe 활용 -->
<form id="csrfForm" action="http://localhost:8080/board/new" method="POST" target="hiddenFrame">
<input type="hidden" name="title" value="Form CSRF ATTACK">
<input type="hidden" name="writer" value="hacker">
<input type="hidden" name="content" value="게시판 중학생한테 털렸죠 ㅋㅋㅋㅋㅋ">
</form>
<!-- iframe 추가 - 폼 제출 결과 여기로 이동 (사용자가 공격 사실 모르도록) -->
<iframe name="hiddenFrame" style="display:none;"></iframe>
<script>
// 노출되지 않은 form에서 진행된 요청 전송
document.getElementById('csrfForm').submit();
</script>
form을 submit할 경우 자동으로 페이지 이동이 진행되는데
hiddenFrame을 생성하여 페이지 이동을 hiddenFrame에서 발생시켜 사용자가 알아차리지 못하도록 하였다.
2. ajax를 활용한 CSRF 공격
두번째 방법은 ajax를 활용하여 페이지 로딩 시 게시글이 작성되도록 하는 방식이다.
<!-- 방법 2: ajax를 활용한 CSRF 공격 -->
<!-- 요청 시 credentials: 'include' 옵션을 추가하여 쿠키(세션) 포함 -->
function sendCSRF() {
var formData = new FormData();
formData.append('title', 'JS CSRF ATTACK');
formData.append('writer', 'hacker');
formData.append('content', '게시판 중학생한테 털렸죠 ㅋㅋㅋㅋㅋ');
fetch('http://localhost:8080/board/new', {
method: 'POST',
body: formData,
credentials: 'include', // 쿠키(세션) 포함
}).then((result) => {
document.getElementById('status').innerHTML = '<p style="color: green;">AJAX 요청 전송 완료</p>';
});
}
sendCSRF();
게시판 서비스에서 사용자의 인증된 세션 정보는 쿠키에 저장하고 있으므로 ajax 요청 시 인증 정보도 같이 전달하기 위해 credentials: 'include' 옵션을 추가해주었다.
실행 결과

위와 같이 공격자는 게시판 서비스에서 접근 권한이 없음에도 사용자 인증 정보를 활용하여 글을 등록할 수 있었다.
그렇다면 이는 어떻게 방지할 수 있을까??
대처 방안
1. 쿠키 SameSite 정책을 설정
첫번째 방법은 공격자의 사이트에서 사용자 인증 정보를 활용할 수 없도록 하는 것이다.
이를 위하여 우리는 사용자 인증 정보 쿠키에 SameSite 속성을 추가하면 된다.
SameSite 속성이란?
브라우저가 해당 쿠키를 어떤 상황에서 요청에 포함시킬지를 제어하는 정책
SameSite 속성은 총 3종류가 있다.
1. SameSite = None
모든 cross-site 요청에도 쿠키를 전송
제3자 서비스에서도 쿠키를 사용할 수 있기 때문에 위와 같은 CSRF 공격에 취약하다.
2. SameSite=Lax (기본값)
비교적 보안상 안전한 GET 방식의 요청에서만 쿠키를 전송
POST 요청, iframe, 이미지, AJAX 등 cross-site 컨텍스트에서는 쿠키를 전송하지 않음.
Chrome 80 버전부터는 별도의 정책이 수립되어있지 않은 경우 Lax를 기본값으로 사용한다.
(localhost의 경우 브라우저에서 테스트 환경으로 판단하여 이 정책에 포함되지 않았다)
3. SameSite=Strict
가장 보수적인 설정으로 같은 사이트 내에서의 요청에서만 해당 쿠키를 사용할 수 있다.
예시에서 CSRF 공격은 iframe, AJAX 요청을 통해 진행되었으므로 SameSite를 Lax로 설정하여 CSRF 공격을 예방할 수 있다.


인증 정보 쿠키가 없으므로 글이 등록되지 않고 login 페이지로 redirect 되는 것을 확인할 수 있다.

2. CSRF 토큰 활용
두번째 방법은 CSRF 토큰을 활용하는 것이다.
CSRF 토큰이란 서버에서 암호화된 랜덤값을 생성 후 브라우저와 주고 받아 사용자의 의도에 따라 정상적으로 발생된 요청인지 검증하는데 쓰이는 토큰값이다.
글을 시작할 때 보았던 Spring Security의 CSRF 속성은 CSRF 토큰을 활용할지에 대한 설정값이다.

해당 설정값을 disable 하지 않을 경우 POST 요청 시 CSRF 토큰이 같이 넘어왔는지 검증을 진행하고 토큰이 없을 경우 요청은 거부된다.

아래와 같이 form에 CSRF 토큰 값 추가하여 같이 전송해주면 정상적으로 요청이 처리된다.


REST API에서 CSRF 토큰을 disable 하는 이유?
REST API의 경우 무상태(stateless)라 이전 요청에서 발생한 내용을 기억하지 않는다.
쿠키나 세션에 인증 정보를 저장하지 않기 때문에 브라우저에 저장된 인증 정보를 악용하는 CSRF 공격에 당할 위험이 적다.
따라서 Spring Security의 CSRF 토큰 설정값을 disable 할 수 있는 것이다.