🎯 CSRF(Cross-Site Request Forgery)란?
CSRF는 사용자가 의도하지 않은 요청을 공격자가 만든 웹사이트를 통해 실행시키는 공격입니다.
사용자가 이미 로그인되어 있는 사이트에서, 공격자가 조작한 요청을 사용자 몰래 전송하게 만듭니다.
공격 시나리오 예시
- 사용자가 은행 사이트(
bank.com)에 로그인되어 있음
- 공격자가 만든 악성 사이트(
evil.com)를 방문
- 악성 사이트에 숨겨진 코드가
bank.com으로 송금 요청을 전송
- 브라우저는 자동으로 쿠키를 포함해 요청을 전송
- 은행 서버는 정상적인 요청으로 인식하고 처리
🔍 CSRF 공격의 핵심 원리
브라우저의 쿠키 자동 전송
브라우저는 같은 도메인으로의 모든 요청에 자동으로 쿠키를 첨부합니다.
GET /transfer?to=attacker&amount=10000 HTTP/1.1 Host: bank.com Cookie: session_id=abc123 ← 자동으로 포함됨!
|
공격 코드 예시
1. 이미지 태그를 이용한 공격
<img src="https://bank.com/transfer?to=attacker&amount=10000" style="display:none">
|
2. 자동 제출 폼을 이용한 공격
<form action="https://bank.com/transfer" method="POST" id="hack"> <input type="hidden" name="to" value="attacker"> <input type="hidden" name="amount" value="10000"> </form>
<script> document.getElementById('hack').submit(); </script>
|
3. AJAX를 이용한 공격
fetch('https://bank.com/transfer', { method: 'POST', credentials: 'include', body: JSON.stringify({ to: 'attacker', amount: 10000 }) });
|
🛡️ CSRF 방어 기법
1. CSRF Token (가장 기본적이고 강력한 방법)
서버가 생성한 랜덤한 토큰을 폼에 포함시키고, 요청 시 검증합니다.
서버 측 구현 (Spring Security 예시)
@Configuration @EnableWebSecurity public class SecurityConfig {
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) ); return http.build(); } }
|
클라이언트 측 구현
<form action="/transfer" method="POST"> <input type="hidden" name="_csrf" value="${csrfToken}"> <input type="text" name="to"> <input type="number" name="amount"> <button type="submit">송금</button> </form>
|
const csrfToken = document.querySelector('meta[name="_csrf"]').content;
fetch('/transfer', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }, body: JSON.stringify({ to: 'recipient', amount: 1000 }) });
|
2. SameSite Cookie 속성
브라우저가 외부 사이트에서의 요청에는 쿠키를 전송하지 않도록 설정합니다.
Set-Cookie: session_id=abc123; SameSite=Strict; HttpOnly; Secure
|
SameSite 옵션
| 값 |
설명 |
Strict |
외부 사이트에서 온 요청에는 절대 쿠키를 전송하지 않음 |
Lax |
GET 요청 등 일부 안전한 요청에만 쿠키 전송 (기본값) |
None |
모든 요청에 쿠키 전송 (HTTPS + Secure 속성 필수) |
Spring Boot 설정
server: servlet: session: cookie: same-site: strict http-only: true secure: true
|
@Bean public CookieSameSiteSupplier cookieSameSiteSupplier() { return CookieSameSiteSupplier.ofStrict(); }
|
3. Referer/Origin 헤더 검증
요청이 신뢰할 수 있는 도메인에서 왔는지 확인합니다.
@Component public class CsrfRefererFilter extends OncePerRequestFilter {
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String referer = request.getHeader("Referer"); String origin = request.getHeader("Origin");
if (!"POST".equals(request.getMethod())) { chain.doFilter(request, response); return; }
if (referer == null || !referer.startsWith("https://bank.com")) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid referer"); return; }
chain.doFilter(request, response); } }
|
4. Double Submit Cookie 패턴
CSRF 토큰을 쿠키와 요청 파라미터 양쪽에 포함시키고, 서버에서 두 값이 일치하는지 확인합니다.
function getCookie(name) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop().split(';').shift(); }
const csrfToken = getCookie('XSRF-TOKEN');
fetch('/api/transfer', { method: 'POST', headers: { 'X-XSRF-TOKEN': csrfToken, 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
|
AJAX 요청에 커스텀 헤더를 추가하는 방법입니다.
Same-Origin Policy에 의해 외부 사이트는 커스텀 헤더를 추가할 수 없습니다.
fetch('/api/transfer', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
|
if (!"XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; }
|
🔐 실전 방어 전략
권장 조합
1. CSRF Token (Stateful) 2. SameSite=Strict Cookie 3. Referer/Origin 헤더 검증 4. HTTPS 강제
1. Double Submit Cookie 2. SameSite=Lax Cookie 3. Custom Header 검증 4. CORS 정책 엄격히 설정
|
Spring Security + React 예시
Backend (Spring Boot)
@Configuration @EnableWebSecurity public class SecurityConfig {
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) ) .cors(cors -> cors.configurationSource(corsConfigurationSource()));
return http.build(); }
@Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of("https://frontend.com")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); config.setAllowCredentials(true); config.setAllowedHeaders(List.of("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; } }
@RestController public class CsrfController {
@GetMapping("/api/csrf-token") public CsrfToken getCsrfToken(CsrfToken token) { return token; } }
|
Frontend (React)
import axios from 'axios';
const api = axios.create({ baseURL: 'https://api.bank.com', withCredentials: true });
async function getCsrfToken() { const response = await api.get('/api/csrf-token'); return response.data.token; }
api.interceptors.request.use(async (config) => { if (['post', 'put', 'delete'].includes(config.method)) { const token = await getCsrfToken(); config.headers['X-CSRF-TOKEN'] = token; } return config; });
export async function transfer(to, amount) { const response = await api.post('/api/transfer', { to, amount }); return response.data; }
|
⚠️ 주의사항
CSRF 방어가 필요 없는 경우
순수 읽기 전용 API (GET)
- 단, GET 요청으로 상태를 변경하면 안 됨!
상태를 변경하지 않는 요청
GET 요청의 위험성
@GetMapping("/delete-account") public String deleteAccount() { }
@PostMapping("/delete-account") @CsrfProtected public String deleteAccount() { }
|
API 인증 방식별 CSRF 취약점
| 인증 방식 |
CSRF 취약점 |
이유 |
| Session Cookie |
✅ 취약 |
브라우저가 자동으로 쿠키 전송 |
| JWT (Cookie) |
✅ 취약 |
쿠키 저장 시 자동 전송 |
| JWT (LocalStorage) |
❌ 안전 |
JavaScript로만 접근 가능 (XSS 주의) |
| Bearer Token |
❌ 안전 |
Authorization 헤더는 자동 전송 안 됨 |
🧪 CSRF 공격 테스트
취약점 확인 방법
<!DOCTYPE html> <html> <head> <title>CSRF Test</title> </head> <body> <h1>CSRF Attack Simulation</h1> <form action="https://target.com/api/transfer" method="POST" id="attackForm"> <input type="hidden" name="to" value="attacker"> <input type="hidden" name="amount" value="10000"> </form>
<script> document.getElementById('attackForm').submit(); </script> </body> </html>
|
방어 확인 방법
curl -X POST https://bank.com/api/transfer \ -H "Cookie: session_id=abc123" \ -d "to=attacker&amount=10000"
curl -X POST https://bank.com/api/transfer \ -H "Cookie: session_id=abc123" \ -H "X-CSRF-TOKEN: invalid_token" \ -d "to=attacker&amount=10000"
curl -X POST https://bank.com/api/transfer \ -H "Cookie: session_id=abc123" \ -H "X-CSRF-TOKEN: valid_token_xyz" \ -d "to=recipient&amount=1000"
|
✅ 체크리스트
🎓 결론
CSRF는 브라우저의 쿠키 자동 전송 특성을 악용한 공격입니다.
핵심 방어 전략:
- CSRF Token 또는 Double Submit Cookie 사용
- SameSite 쿠키 속성 설정
- CORS 정책 엄격히 관리
- GET 요청으로 상태 변경 금지
단일 방어 기법보다는 여러 기법을 조합하여 다층 방어(Defense in Depth)를 구축하는 것이 가장 안전합니다.