내 계정으로 돈이 송금됐다? CSRF 공격 원리와 3중 방어막 구축하기
로그인은 되어 있는데, 내가 누르지도 않은 ‘회원 탈퇴’ 버튼이 실행된다면 어떨까요? **CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)**는 사용자가 웹사이트에 로그인되어 있다는 ‘신뢰’를 이용해 공격자가 사용자의 브라우저를 조종하는 비열한 공격 방식입니다.
오늘은 CSRF의 작동 원리를 파헤치고, 프론트엔드와 백엔드에서 이를 어떻게 철벽 방어할 수 있는지 알아보겠습니다.
📌 목차
- CSRF란 무엇인가?
- CSRF 공격의 성립 조건
- 실전 코드 예시: 공격은 어떻게 이루어지는가?
- CSRF를 막는 3가지 핵심 방어 전략
- 개발자 팁: SameSite 속성의 오해와 진실
- 자주 묻는 질문(FAQ)
CSRF란 무엇인가?
CSRF는 사용자가 자신의 의지와 무관하게 공격자가 의도한 행위(데이터 수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 만드는 공격입니다. 공격자는 사용자의 **인증된 세션(쿠키)**을 이용합니다. 브라우저는 특정 사이트에 요청을 보낼 때 해당 사이트의 쿠키를 자동으로 포함시킨다는 점을 악용하는 것이죠.
CSRF 공격의 성립 조건
공격이 성공하려면 다음 세 가지가 충족되어야 합니다.
- 로그인 상태: 사용자가 타겟 사이트에 로그인되어 있어 브라우저에 인증 쿠키가 남아있어야 합니다.
- 예측 가능한 파라미터: 공격자가 요청을 위조하기 위해 API의 구조(URL, 파라미터 이름 등)를 미리 알고 있어야 합니다.
- 사용자의 유도: 사용자가 공격자가 만든 악성 페이지나 링크를 클릭해야 합니다.
실전 코드 예시
공격자가 어떻게 교묘하게 요청을 보내는지 확인해 봅시다.
❌ 공격자의 악성 페이지 예시
공격자는 본인의 사이트에 다음과 같은 숨겨진 폼을 만들어 둘 수 있습니다.
<html>
<body>
<h1>축하합니다! 경품에 당첨되셨습니다.</h1>
<p>아래 버튼을 눌러 경품을 확인하세요.</p>
<form id="attack-form" action="[https://my-bank.com/api/transfer](https://my-bank.com/api/transfer)" method="POST">
<input type="hidden" name="to" value="attacker_account" />
<input type="hidden" name="amount" value="1000000" />
</form>
<script>
// 페이지가 로드되자마자 폼을 자동으로 제출
document.getElementById('attack-form').submit();
</script>
</body>
</html>
사용자가 이 페이지를 방문하는 순간, 브라우저는 my-bank.com으로 송금 요청을 보냅니다. 이때 사용자의 로그인 쿠키가 함께 전송되므로, 서버는 이를 정상적인 사용자의 요청으로 판단하고 처리를 완료해 버립니다.
CSRF를 막는 3가지 핵심 방어 전략
1. SameSite 쿠키 속성 (가장 추천)
쿠키를 설정할 때 SameSite 속성을 사용하면 브라우저가 “다른 사이트에서 발생한 요청”에 쿠키를 포함할지 결정합니다.
- Strict: 동일한 도메인에서만 쿠키 전송. (가장 안전하지만 사용자 경험 저하 가능)
- Lax (기본값): 서드파티 쿠키 전송을 제한하지만, 외부에서 링크를 타고 들어오는
GET요청 등은 허용합니다. 대부분의 CSRF를 방어합니다.
// 백엔드 쿠키 설정 예시 (Node.js/Express)
res.cookie('sessionId', 'abc12345', {
httpOnly: true,
secure: true, // HTTPS 필수
sameSite: 'lax', // CSRF 방어의 핵심
});
2. CSRF 토큰 (CSRF Token)
서버가 페이지를 렌더링할 때 사용자마다 고유한 임시 토큰을 발급하고, 요청 시 이 토큰이 포함되어 있는지 확인하는 방식입니다. 공격자는 사용자의 쿠키는 가질 수 있지만, 서버가 발행한 이 ‘무작위 토큰’ 값은 알 수 없습니다.
// 프론트엔드 요청 시 헤더에 토큰 포함 (Axios 예시)
axios.post('/api/update', data, {
headers: {
'X-CSRF-TOKEN': csrfTokenFromServer // 서버로부터 받은 토큰
}
});
3. Custom Header 검증
공격자의 사이트에서 form 태그로 보내는 요청은 커스텀 HTTP 헤더를 추가할 수 없습니다. 따라서 서버에서 X-Requested-With 같은 헤더의 존재 여부를 체크하는 것만으로도 단순 폼 기반 CSRF를 막을 수 있습니다.
개발자 팁: SameSite 속성의 오해와 진실
💡 SameSite=Lax면 무조건 안전한가요? 아니요!
Lax는POST,PUT,DELETE등 상태를 변경하는 요청은 막아주지만,GET요청은 막지 않습니다. 만약 여러분의 API 중 중요 로직(예: 회원탈퇴)이GET방식으로 설계되어 있다면,SameSite=Lax환경에서도 CSRF 공격에 노출될 수 있습니다. 상태 변경 작업은 반드시 POST 이상의 메서드를 사용하세요.
FAQ
Q1. XSS와 CSRF의 가장 큰 차이점은 무엇인가요?
XSS는 사용자의 브라우저 내에서 악성 코드를 실행하는 것이 목표라면, CSRF는 사용자의 권한을 이용해 원치 않는 요청을 보내게 만드는 것이 목표입니다. XSS는 사이트 자체의 취약점을 이용하고, CSRF는 브라우저의 쿠키 전송 메커니즘을 이용합니다.
Q2. API 서버만 따로 운영하는 경우(CORS)에도 CSRF가 발생하나요?
네, 발생합니다. CORS 설정(Access-Control-Allow-Origin)은 브라우저가 응답 데이터를 읽는 것을 제한할 뿐, 서버로 요청이 전달되어 실행되는 것 자체를 막지는 못합니다. 따라서 API 서버에서도 반드시 CSRF 방어 로직이 필요합니다.
Q3. JWT를 사용하면 CSRF로부터 안전한가요?
JWT를 쿠키에 저장한다면 일반 세션 방식과 동일하게 CSRF에 취약합니다. 하지만 JWT를 Authorization 헤더(Bearer 토큰)에 담아 보낸다면, 브라우저가 자동으로 전송하지 않으므로 CSRF 공격으로부터는 안전해집니다. (대신 XSS를 통한 토큰 탈취를 더 주의해야 합니다.)
마무리하며
CSRF는 현대 브라우저의 SameSite 기본값 변경 덕분에 예전보다 방어하기 쉬워졌습니다. 하지만 여전히 구형 브라우저 대응이나 잘못된 API 설계로 인한 허점은 존재합니다. SameSite 쿠키를 기본으로 쓰되, 중요한 액션에는 CSRF 토큰을 병행하는 것이 가장 안전한 표준입니다.
더 깊은 보안 지식은 OWASP CSRF 방어 치트시트에서 확인해 보세요.