상위: Web Security
요약
CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)는 사용자가 의도하지 않은 요청을 실행하게 만드는 공격입니다. 피해자의 인증된 세션을 이용해 비밀번호 변경, 송금 등의 악성 행위를 수행합니다.
공격 원리
1. 피해자가 은행 사이트에 로그인 (세션 쿠키 발급)
2. 피해자가 공격자의 악성 사이트 방문
3. 악성 사이트에서 은행으로 송금 요청 전송
4. 브라우저가 자동으로 세션 쿠키 포함하여 요청
5. 은행은 정상 요청으로 인식하여 처리
공격자 악성 사이트 피해자 브라우저 은행 서버
│ │ │
│ ← 악성 페이지 로드 ← │ │
│ │ │
│ │ ─── 송금 요청 ────────▶ │
│ │ (세션 쿠키 자동 포함) │
│ │ │
│ │ ◀─── 송금 성공 ─────── │
공격 시나리오
시나리오 1: 비밀번호 변경
정상적인 비밀번호 변경 폼:
<form action="https://bank.com/change-password" method="POST">
<input name="new_password" value="새비밀번호">
<button>변경</button>
</form>
공격자의 악성 페이지:
<!-- 피해자가 이 페이지를 열면 자동으로 비밀번호 변경 -->
<body onload="document.forms[0].submit()">
<form action="https://bank.com/change-password" method="POST">
<input name="new_password" value="hacker123">
</form>
</body>
시나리오 2: 송금
<!-- 이미지 태그로 GET 요청 -->
<img src="https://bank.com/transfer?to=attacker&amount=1000000" style="display:none">
<!-- 또는 숨겨진 폼으로 POST 요청 -->
<iframe name="csrf-frame" style="display:none"></iframe>
<form action="https://bank.com/transfer" method="POST" target="csrf-frame">
<input name="to" value="attacker">
<input name="amount" value="1000000">
</form>
<script>document.forms[0].submit();</script>
시나리오 3: 관리자 계정 생성
<form action="https://admin.company.com/create-admin" method="POST">
<input name="username" value="hacker">
<input name="password" value="hacker123">
<input name="role" value="admin">
</form>
<script>document.forms[0].submit();</script>
XSS vs CSRF 비교
| 구분 | XSS | CSRF |
|---|---|---|
| 공격 위치 | 피해자 브라우저에서 스크립트 실행 | 피해자의 인증 세션 이용 |
| 필요 조건 | 스크립트 삽입 가능한 취약점 | 피해자가 로그인 상태 |
| 할 수 있는 일 | 모든 것 (쿠키 탈취, DOM 조작 등) | 인증된 요청만 가능 |
| 응답 확인 | 가능 | 불가능 (Same-Origin Policy) |
방어 방법
1. CSRF Token (가장 효과적)
서버에서 토큰 생성:
# Django
from django.middleware.csrf import get_token
def form_view(request):
csrf_token = get_token(request)
return render(request, 'form.html', {'csrf_token': csrf_token})
폼에 토큰 포함:
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="abc123xyz">
<input name="to" placeholder="받는 사람">
<input name="amount" placeholder="금액">
<button>송금</button>
</form>
서버에서 검증:
def transfer(request):
if request.POST['csrf_token'] != request.session['csrf_token']:
return HttpResponse("CSRF 공격 탐지", status=403)
# 정상 처리
원리:
공격자는 csrf_token 값을 알 수 없음 (Same-Origin Policy)
→ 올바른 토큰 없이는 요청 실패
2. SameSite 쿠키
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
| 값 | 설명 | CSRF 방어 |
|---|---|---|
| Strict | 외부 사이트에서 쿠키 전송 안 함 | 완벽 |
| Lax | GET 요청은 허용, POST는 차단 | 부분적 |
| None | 항상 전송 (Secure 필수) | 없음 |
SameSite=Strict 설정 시:
공격자 사이트 → bank.com 요청 → 쿠키 미포함 → 인증 실패
3. Referer/Origin 헤더 검증
def transfer(request):
origin = request.headers.get('Origin')
referer = request.headers.get('Referer')
allowed_origins = ['https://bank.com', 'https://www.bank.com']
if origin not in allowed_origins:
return HttpResponse("CSRF 공격 탐지", status=403)
한계:
- Referer 헤더가 없을 수 있음 (프라이버시 설정)
- 우회 가능한 경우 존재
4. Double Submit Cookie
// 쿠키와 헤더에 동일한 값 전송
document.cookie = "csrf_token=abc123";
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': 'abc123' // 헤더로도 전송
},
body: JSON.stringify({to: 'friend', amount: 10000})
});
원리:
- 공격자는 Same-Origin Policy로 인해 쿠키 값을 읽을 수 없음
- 헤더에 올바른 값을 설정할 수 없음
5. 중요 작업에 재인증 요구
def transfer(request):
# 송금 전 비밀번호 재확인
if not verify_password(request.POST['password']):
return HttpResponse("비밀번호 확인 필요", status=403)
프레임워크별 CSRF 보호
Django
# settings.py
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware', # 기본 활성화
]
# 템플릿
<form method="POST">
{% csrf_token %}
...
</form>
Spring
// 기본 활성화 (Spring Security)
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http.csrf(); // 기본 활성화
return http.build();
}
}
Express.js
const csrf = require('csurf');
app.use(csrf({ cookie: true }));
app.get('/form', (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
테스트 방법
1. 토큰 제거 테스트
1. 정상 요청에서 CSRF 토큰 제거
2. 요청이 성공하면 취약
2. 토큰 재사용 테스트
1. 유효한 토큰을 다른 세션에서 사용
2. 요청이 성공하면 취약
3. 메서드 변경 테스트
POST → GET으로 변경하여 테스트
GET 요청에는 CSRF 보호가 없을 수 있음
CSRF 공격이 불가능한 경우
- API 토큰 인증: Bearer Token은 자동 전송되지 않음
- Custom Header 필수:
X-Requested-With등 커스텀 헤더 필요 시 - JSON Content-Type: 일반 폼으로 JSON 전송 불가
관련 공격
- XSS - CSRF Token 탈취에 악용 가능
- Clickjacking - 클릭 유도로 CSRF 수행
- Session Hijacking - 세션 탈취
- CORS - Cross-Origin 정책 이해