CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)는 로그인한 사용자가 악성 사이트를 방문했을 때, 그 사이트가 사용자의 브라우저를 시켜 사용자 모르게 원래 사이트에 요청을 보내게 하는 공격이다. 공격자는 토큰을 훔치지 않는다. 브라우저가 쿠키를 알아서 붙여 준다는 점을 이용한다.
왜 가능한가
<!-- evil.example 페이지 안 -->
<form action="https://bank.example/transfer" method="POST">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="1000000">
</form>
<script>document.forms[0].submit()</script>- 브라우저는
bank.example로 가는 요청에 그 사이트의 쿠키(세션 ID, session identifier)를 자동으로 붙인다. 서버는 정상 로그인 사용자의 요청으로 본다 - 동일 출처 정책(Same-Origin Policy, SOP)은 다른 출처의 응답을 읽는 것은 막지만, 폼 전송·이미지 요청처럼 보내는 것은 막지 않는다. CSRF는 응답을 읽을 필요가 없다
- 그래서 쿠키로 인증하는 세션 인증이 주된 대상이다.
Authorization헤더에 토큰을 직접 넣는 방식은 브라우저가 자동으로 붙이지 않아 상대적으로 안전하다(JWT)
방어
- CSRF 토큰: 서버가 세션별로 추측할 수 없는 값을 발급하고, 상태를 바꾸는 요청에 폼 필드나 헤더로 함께 보내게 한다. 공격자는 이 값을 읽을 수 없어 맞출 수 없다. 서버 저장 방식(synchronizer token)과 쿠키·요청 값을 비교하는 방식(double submit)이 있다(Django의 CSRF 방어)
- SameSite 쿠키:
SameSite=Lax(크롬 계열은 속성이 없는 쿠키도 Lax로 다루지만 모든 브라우저의 기본은 아니다)는 다른 사이트에서 시작한 POST 요청에 쿠키를 싣지 않는다.Strict는 링크 이동에도 안 싣는다. 강력한 기본 방어지만, 하위 도메인이나 오래된 브라우저 같은 예외가 있어 토큰과 함께 쓴다 - Origin·Referer 검사: 요청 헤더의 출처가 내 사이트인지 확인한다(Origin·Referer 헤더와 Referrer-Policy)
- GET은 안전하게: 상태를 바꾸는 동작을 GET으로 만들지 않는다. 이미지 태그 하나로도 요청이 나간다
프레임워크 기본값
- 스프링 시큐리티는 GET·HEAD·OPTIONS·TRACE가 아닌 요청에 CSRF 보호를 기본으로 켠다. 세션 쿠키 없이 토큰만 쓰는 API라면 끄기도 하지만, 근거를 확인하고 끈다(시큐리티 필터체인)
- Django는
CsrfViewMiddleware가 같은 일을 한다
XSS와의 차이
CSRF는 공격자가 사용자 행세를 하는 것이고, XSS(Cross-Site Scripting)는 내 사이트에서 공격자 스크립트가 돌아 브라우저를 장악하는 것이다. XSS가 있으면 페이지 안의 CSRF 토큰도 읽을 수 있어 CSRF 방어가 무력해진다. CORS(Cross-Origin Resource Sharing)는 읽기를 허용하는 장치이지 CSRF 방어가 아니다.
출처: OWASP CSRF Prevention Cheat Sheet: Synchronizer Token Pattern · Double Submit Cookie · SameSite · Origin 검사