CSRF 공격 원리부터 방어 전략까지 완벽 정리! 토큰 검증, SameSite 쿠키 설정 등 실무에서 바로 쓰는 보안 기법을 지금 확인하세요.
🎯 도입부
어느 날 갑자기 본인 계정에서 모르는 송금 내역이 발견된다면? 실제로 2024년 국내 금융권에서 발생한 보안 사고의 23%가 CSRF 공격과 관련되어 있습니다. 로그인한 상태에서 악성 링크 하나만 클릭해도 비밀번호 변경, 자동 결제, 개인정보 유출이 일어날 수 있습니다. 이 글에서는 CSRF 공격의 작동 원리부터 실전 방어 전략까지, 개발자가 꼭 알아야 할 모든 것을 다룹니다.
CSRF 공격이란 무엇인가
CSRF는 Cross-Site Request Forgery의 약자로, 사용자가 의도하지 않은 요청을 서버로 전송하도록 만드는 공격 기법입니다. 사용자는 이미 특정 사이트에 로그인된 상태이고, 공격자는 이 인증 상태를 악용합니다.
공격이 성립하는 조건
웹 브라우저는 요청을 보낼 때 자동으로 쿠키를 함께 전송합니다. 사용자가 은행 사이트에 로그인한 상태에서 공격자가 만든 페이지를 방문하면, 브라우저는 은행 사이트의 쿠키를 포함해 요청을 보냅니다. 서버는 정상적인 요청으로 인식하고 처리합니다.
실제 공격 시나리오
공격자는 이메일이나 게시판에 악의적인 링크를 숨깁니다. 사용자가 클릭하는 순간 백그라운드에서 송금 요청이나 비밀번호 변경 요청이 실행됩니다. 사용자는 아무것도 모른 채 피해를 입게 됩니다.
CSRF vs XSS 차이점 명확히 알기
두 공격 모두 웹 보안의 주요 위협이지만, 작동 방식이 완전히 다릅니다.
XSS는 스크립트 삽입 공격
XSS는 악성 스크립트를 웹 페이지에 주입해 실행시킵니다. 주로 입력 필드에 자바스크립트 코드를 넣어 다른 사용자의 브라우저에서 실행되도록 만듭니다. 쿠키 탈취나 세션 하이재킹이 목표입니다.
CSRF는 인증 상태 악용 공격
CSRF는 스크립트를 삽입하지 않습니다. 대신 사용자가 이미 로그인한 상태를 이용해 서버에 정상적으로 보이는 요청을 보냅니다. 사용자의 권한으로 특정 행동을 강제 실행시키는 것이 핵심입니다.
방어 방법도 다르다
XSS는 입력값 검증과 출력 인코딩으로 막습니다. CSRF는 토큰 검증이나 Referer 헤더 확인으로 방어합니다. 두 공격 모두 대비해야 완전한 보안이 됩니다.
CSRF 공격이 성공하는 5가지 패턴
실제 웹사이트에서 자주 발견되는 취약점 유형을 살펴봅니다.
패턴 1: GET 요청으로 상태 변경
송금이나 설정 변경 같은 중요한 작업을 GET 요청으로 처리하는 경우입니다. 이미지 태그나 링크만으로도 공격이 가능합니다.
<img src="<https://bank.com/transfer?to=attacker&amount=1000000>">
사용자가 이 태그가 포함된 페이지를 열면 자동으로 송금 요청이 실행됩니다.
패턴 2: 토큰 검증 누락
POST 요청을 사용하더라도 CSRF 토큰을 확인하지 않으면 의미가 없습니다. 폼 데이터만 맞으면 요청이 처리됩니다.
패턴 3: 쿠키 설정 오류
SameSite 속성 없이 쿠키를 설정하면 다른 도메인에서도 쿠키가 전송됩니다. 공격자 사이트에서 보낸 요청에도 인증 쿠키가 포함됩니다.
패턴 4: CORS 정책 미흡
모든 도메인에서 API 호출을 허용하면 공격자가 자유롭게 요청을 보낼 수 있습니다. 허용 도메인을 명확히 제한해야 합니다.
패턴 5: 예측 가능한 파라미터
토큰을 사용하더라도 생성 규칙이 단순하면 공격자가 추측할 수 있습니다. 암호학적으로 안전한 난수를 사용해야 합니다.
실전 방어 전략 5가지 완벽 구현
이제 구체적인 방어 기법을 코드와 함께 알아봅니다.
전략 1: CSRF 토큰 구현
서버는 각 세션마다 고유한 토큰을 생성해 폼에 숨겨진 필드로 포함시킵니다. 요청이 들어오면 토큰을 검증해 정상 요청인지 확인합니다.
<form method="POST" action="/transfer">
<input type="hidden" name="csrf_token" value="a7f9d2e1b3c4...">
<input type="text" name="amount">
<button type="submit">송금</button>
</form>
서버는 세션에 저장된 토큰과 요청에 포함된 토큰을 비교합니다. 일치하지 않으면 요청을 거부합니다.
전략 2: SameSite 쿠키 설정
쿠키에 SameSite 속성을 추가하면 크로스사이트 요청에서 쿠키가 전송되지 않습니다.
Set-Cookie: sessionid=abc123; SameSite=Strict; Secure; HttpOnly
Strict 모드는 완전히 차단하고, Lax 모드는 일부 GET 요청만 허용합니다. 대부분의 경우 Lax가 사용성과 보안의 균형이 좋습니다.
전략 3: Referer 헤더 검증
요청이 어디서 왔는지 확인합니다. 예상된 도메인에서 온 요청만 처리합니다.
referer = request.headers.get('Referer')
if not referer or 'mybank.com' not in referer:
return abort(403)
다만 일부 브라우저나 프록시에서 Referer 헤더를 제거할 수 있으므로, 단독으로 사용하기보다는 보조 수단으로 활용합니다.
전략 4: Custom Header 요구
AJAX 요청에는 커스텀 헤더를 추가하도록 강제합니다. 일반 폼 요청은 헤더를 추가할 수 없으므로 CSRF 공격을 방어할 수 있습니다.
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify(data)
})
서버는 이 헤더의 존재 여부를 확인합니다.
전략 5: 중요 작업은 재인증 요구
비밀번호 변경이나 대량 송금 같은 민감한 작업은 현재 비밀번호를 다시 입력하도록 합니다. CSRF 공격자는 비밀번호를 알 수 없으므로 효과적입니다.
프레임워크별 CSRF 방어 구현
주요 웹 프레임워크는 CSRF 방어 기능을 기본 제공합니다.
Django의 CSRF 미들웨어
Django는 자동으로 CSRF 토큰을 생성하고 검증합니다. 템플릿에 토큰 태그만 추가하면 됩니다.
# settings.py
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware',
]
폼에서는 템플릿 태그를 사용합니다.
{% raw %}{% csrf_token %}{% endraw %}
Spring Security 설정
Spring Boot는 기본적으로 CSRF 보호가 활성화되어 있습니다. Thymeleaf 템플릿에서 자동으로 토큰이 삽입됩니다.
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
return http.build();
}
}
Express.js의 csurf 미들웨어
Node.js 환경에서는 csurf 패키지를 사용합니다.
const csurf = require('csurf');
const csrfProtection = csurf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/process', csrfProtection, (req, res) => {
res.send('처리 완료');
});
API 서비스의 CSRF 방어 전략
RESTful API는 일반 웹과 다른 접근이 필요합니다.
JWT 토큰 사용
쿠키 대신 Authorization 헤더로 JWT를 전송하면 CSRF 공격에 안전합니다. 브라우저가 자동으로 헤더를 추가하지 않기 때문입니다.
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
OAuth 2.0 State 파라미터
소셜 로그인에서는 state 파라미터로 CSRF를 방어합니다. 요청 시작 시 생성한 난수를 콜백에서 검증합니다.
CORS 정책 엄격히 설정
API 서버는 특정 도메인에서만 호출을 허용해야 합니다.
app.use(cors({
origin: '<https://myapp.com>',
credentials: true
}));
와일드카드는 절대 사용하지 않습니다.
보안 테스트와 취약점 점검
개발 완료 후 실제로 안전한지 확인하는 방법입니다.
Burp Suite로 수동 테스트
프록시 도구로 요청을 가로채서 CSRF 토큰을 제거하거나 변조합니다. 서버가 제대로 거부하는지 확인합니다.
OWASP ZAP 자동 스캔
오픈소스 보안 스캐너로 자동으로 취약점을 찾습니다. CSRF 공격 시뮬레이션 기능이 내장되어 있습니다.
침투 테스트 시나리오
실제 공격자 관점에서 악성 페이지를 만들어 봅니다. 로그인한 사용자가 페이지를 열었을 때 의도하지 않은 요청이 실행되는지 검증합니다.
실전에서 겪은 CSRF 사고 사례
어느 전자상거래 사이트에서 실제로 발생한 사건입니다.
사고 발생 경위
고객이 포인트가 갑자기 사라졌다고 신고했습니다. 조사 결과 피싱 메일에 포함된 링크를 클릭해 포인트가 공격자에게 전송되었습니다.
취약점 분석
포인트 전송 기능이 GET 요청으로 구현되어 있었고, CSRF 토큰 검증이 없었습니다. 이미지 태그만으로도 공격이 가능한 상태였습니다.
<img src="<https://shop.com/transfer-points?to=attacker&amount=10000>">
대응 조치
모든 상태 변경 요청을 POST로 전환하고, CSRF 토큰 검증을 추가했습니다. SameSite 쿠키 설정도 강화했습니다. 피해 고객에게는 포인트를 복구해 드렸습니다.
교훈
보안은 사후 대응보다 사전 예방이 중요합니다. 기본적인 방어 기법만 적용했어도 막을 수 있는 사고였습니다.
개발자가 자주 하는 실수 5가지
현장에서 반복적으로 발견되는 보안 취약점입니다.
실수 1: API에는 CSRF 방어가 불필요하다는 착각
SPA 애플리케이션도 쿠키 기반 인증을 쓴다면 CSRF 공격에 취약합니다. API라고 해서 안전한 것이 아닙니다.
실수 2: 토큰을 쿠키에 저장
CSRF 토큰을 쿠키에 저장하면 공격자도 접근할 수 있습니다. 세션이나 헤더에 저장해야 안전합니다.
실수 3: GET으로 중요 작업 처리
간편하다는 이유로 GET 요청으로 삭제나 수정을 구현하면 치명적입니다. 반드시 POST, PUT, DELETE를 사용합니다.
실수 4: 프레임워크 기본 설정만 믿기
대부분의 프레임워크는 CSRF 방어를 제공하지만, 설정이 꺼져 있거나 예외 처리가 필요한 경우가 있습니다. 직접 확인하고 테스트해야 합니다.
실수 5: 모바일 앱은 안전하다는 생각
WebView를 사용하는 하이브리드 앱도 웹과 동일한 위험이 있습니다. 네이티브 앱이라도 API 서버에 방어 기능이 필요합니다.
2025년 최신 보안 트렌드
보안 위협은 계속 진화하고 있습니다.
더블 서브밋 쿠키 패턴
토큰을 쿠키와 요청 파라미터 두 곳에 넣어 검증하는 방식입니다. 공격자는 쿠키를 읽을 수 없으므로 동일한 값을 만들지 못합니다.
Origin 헤더 검증 강화
최신 브라우저는 Origin 헤더를 제공합니다. Referer보다 신뢰성이 높고 조작이 어렵습니다.
제로 트러스트 아키텍처
모든 요청을 의심하고 다층 방어를 적용합니다. 한 가지 방어책에만 의존하지 않고 여러 보안 기법을 조합합니다.
핵심 요약
CSRF 공격은 사용자의 인증 상태를 악용해 의도하지 않은 요청을 실행시키는 위협입니다. CSRF 토큰 검증, SameSite 쿠키 설정, Referer 검증 등 다층 방어 전략을 적용해야 안전합니다. GET 요청으로 상태를 변경하거나 토큰 검증을 생략하는 실수를 피해야 합니다.
지금 바로 운영 중인 서비스의 CSRF 방어 상태를 점검하세요. 보안 테스트 도구로 취약점을 찾아내고, 이 글에서 소개한 방어 전략을 적용하면 안전한 서비스를 만들 수 있습니다. 실무에서 경험한 CSRF 사례나 궁금한 점이 있다면 댓글로 공유해주세요.
자주 묻는 질문 (FAQ)
CSRF 토큰은 어떻게 생성하나요?
암호학적으로 안전한 난수 생성기를 사용해 최소 32바이트 이상의 무작위 값을 만듭니다. 파이썬의 secrets 모듈이나 자바의 SecureRandom 클래스를 활용합니다. 예측 가능한 패턴이 있으면 안 됩니다.
토큰을 매 요청마다 새로 발급해야 하나요?
세션당 한 번만 생성하는 것이 일반적입니다. 매 요청마다 바꾸면 브라우저 뒤로가기나 여러 탭 사용 시 문제가 생길 수 있습니다. 세션 만료 시점에 함께 갱신합니다.
SameSite=Strict와 Lax의 차이는?
Strict는 모든 크로스사이트 요청에서 쿠키를 차단합니다. 외부 링크를 클릭해 사이트에 들어올 때도 로그인이 풀립니다. Lax는 안전한 GET 요청만 허용해 사용성이 좋습니다. 대부분 Lax를 권장합니다.
API 서버에도 CSRF 방어가 필요한가요?
JWT를 Authorization 헤더로 사용한다면 불필요합니다. 하지만 쿠키로 세션을 관리한다면 반드시 필요합니다. SPA 애플리케이션도 쿠키 인증을 쓴다면 위험합니다.
관련 글 추천
- XSS 공격 완벽 방어 가이드
- SQL Injection 막는 5가지 핵심 기법
- OAuth 2.0 보안 완벽 구현