CSRF 공격의 개념
CSRF(Cross-Site Request Forgery)는 사용자가 모르는 사이에 악의적인 요청을 대신 전송하도록 유도하는 공격 기법입니다. XSS(Cross-Site Scripting)와는 달리, CSRF는 브라우저에 저장된 인증 정보를 활용하여 신뢰받는 사용자의 권한으로 작업을 수행합니다.
공격자는 사용자의 브라우저를 "대리인"으로 삼아, 이메일 발송, 계정 정보 변경, 금융 거래 등 다양한 악성 행위를 실행할 수 있습니다.
공격이 성립되는 조건
CSRF 공격이 성공하려면 다음 두 가지 전제가 충족되어야 합니다:
- 사용자가 대상 사이트에 로그인하여 세션 쿠키가 브라우저에 저장된 상태
- 사용자가 공격자가 준비한 악성 페이지를 동일한 브라우저에서 방문
실제로는 브라우저를 닫아도 세션이 즉시 만료되지 않는 경우가 많아, 사용자가 인지하지 못하는 사이에 공격당할 수 있습니다.
서버 중심의 방어 메커니즘
CSRF 방어는 클라이언트보다 서버 측에서 처리하는 것이 효과적입니다. 주요 방어 기법은 다음과 같습니다:
1. HTTP 메서드 제한
데이터 변경 작업은 POST, PUT, DELETE 등의 메서드로 제한하고, GET 요청은 조회용으로만 사용해야 합니다. GET 요청은 <img> 태그 등으로 쉽게 위조될 수 있습니다.
2. HttpOnly 속성 설정
JavaScript를 통한 쿠키 접근을 차단하여 XSS를 통한 쿠키 탈취를 방지합니다.
// Java Servlet 예시
javax.servlet.http.HttpServletResponse resp = ...;
resp.setHeader("Set-Cookie", "sid=abc123; HttpOnly; Secure; SameSite=Strict");
3. Anti-CSRF 토큰 검증
요청마다 예측 불가능한 토큰을 포함시켜, 공격자가 요청을 위조하는 것을 방지합니다.
// 서버 측 토큰 생성 및 검증 예시
import java.util.UUID;
public class CsrfGuard {
private static final String TOKEN_KEY = "_csrf_guard";
public String issueToken(javax.servlet.http.HttpSession session) {
String nonce = UUID.randomUUID().toString();
session.setAttribute(TOKEN_KEY, nonce);
return nonce;
}
public boolean validate(javax.servlet.http.HttpServletRequest req) {
String serverToken = (String) req.getSession().getAttribute(TOKEN_KEY);
String clientToken = req.getParameter(TOKEN_KEY);
return serverToken != null && serverToken.equals(clientToken);
}
}
4. Referer 더 검사
요청의 출처(origin)를 확인하여, 신뢰할 수 있는 도메인에서 온 요청만 처리합니다.
String origin = request.getHeader("Referer");
if (origin == null || !origin.startsWith("https://trusted.example.com")) {
throw new SecurityException("Untrusted request origin");
}
Django의 CSRF 보호 구현
미들웨어 활성화
Django는 기본적으로 CSRF 보호를 제공합니다. settings.py에서 다음 설정이 포함되어 있는지 확인하세요:
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
# CSRF 보호 미들웨어 (기본 활성화)
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
# ...
]
이 미들웨어를 제거하면 CSRF 검증이 비활성화되므로 주의가 필요합니다.
템플릿에서 토큰 삽입
폼 템플릿 내에 {% csrf_token %} 태그를 추가합니다:
<!-- templates/login.html -->
<form method="post" action="{% url 'account:login' %}">
{% csrf_token %}
<input type="text" name="identifier" placeholder="사용자명">
<input type="password" name="secret_key" placeholder="비밀번호">
<button type="submit">로그인</button>
</form>
AJAX 요청 시 토큰 전달
JavaScript로 비동기 요청을 보낼 때도 토큰을 포함해야 합니다:
// static/js/api-client.js
function retrieveCsrfToken() {
const cookieMatch = document.cookie.match(/csrftoken=[^;]+/);
return cookieMatch ? cookieMatch[0].split('=')[1] : null;
}
async function sendSecurePost(endpoint, payload) {
const antiForgeryToken = retrieveCsrfToken();
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': antiForgeryToken
},
credentials: 'same-origin',
body: JSON.stringify(payload)
});
return response.json();
}
Django CSRF 방어의 내부 동작
Django의 CSRF 보호는 다음과 같은 흐름으로 작동합니다:
- 토큰 발급: 사용자가 페이지를 요청하면, 서버는
csrfmiddlewaretoken이라는 이름의 숨겨진 입력 필드를 폼에 삽입합니다. - 쿠키 설정: 동시에 브라우저에는
csrftoken이라는 이름의 쿠키가 저장됩니다. - 이중 검증: 폼 제출 시, 숨겨진 필드 값과 쿠키 값을 모두 서버로 전송합니다.
- 일치 여부 확인: 서버는 두 값을 비교하여 일치할 경우에만 요청을 처리합니다.
이 방식의 핵심은 공격자가 쿠키 값을 읽을 수 없다는 점입니다. 동일 출처 정책(Same-Origin Policy) 덕분에, 악성 사이트에서는 키에 접근할 수 없어 토큰 위조가 불가능합니다.
특정 뷰에서 CSRF 검증 비활성화
API 엔드포oin트 등에서 CSRF 검증을 우회해야 할 경우, 데코레이터를 사용합니다:
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
@csrf_exempt
def external_webhook_receiver(request):
if request.method == 'POST':
# 외부 서비스에서 오는 콜백 처리
payload = request.body.decode('utf-8')
process_notification(payload)
return JsonResponse({'status': 'received'})
단, 이 방식은 보안 위험이 따르므로 반드시 대체 인증 수단(API 키, HMAC 서명 등)을 함께 사용해야 합니다.