Django에서 CSRF 공격 이해 및 방어 전략

CSRF 공격의 개념

CSRF(Cross-Site Request Forgery)는 사용자가 모르는 사이에 악의적인 요청을 대신 전송하도록 유도하는 공격 기법입니다. XSS(Cross-Site Scripting)와는 달리, CSRF는 브라우저에 저장된 인증 정보를 활용하여 신뢰받는 사용자의 권한으로 작업을 수행합니다.

공격자는 사용자의 브라우저를 "대리인"으로 삼아, 이메일 발송, 계정 정보 변경, 금융 거래 등 다양한 악성 행위를 실행할 수 있습니다.

공격이 성립되는 조건

CSRF 공격이 성공하려면 다음 두 가지 전제가 충족되어야 합니다:

  1. 사용자가 대상 사이트에 로그인하여 세션 쿠키가 브라우저에 저장된 상태
  2. 사용자가 공격자가 준비한 악성 페이지를 동일한 브라우저에서 방문

실제로는 브라우저를 닫아도 세션이 즉시 만료되지 않는 경우가 많아, 사용자가 인지하지 못하는 사이에 공격당할 수 있습니다.

서버 중심의 방어 메커니즘

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 보호는 다음과 같은 흐름으로 작동합니다:

  1. 토큰 발급: 사용자가 페이지를 요청하면, 서버는 csrfmiddlewaretoken이라는 이름의 숨겨진 입력 필드를 폼에 삽입합니다.
  2. 쿠키 설정: 동시에 브라우저에는 csrftoken이라는 이름의 쿠키가 저장됩니다.
  3. 이중 검증: 폼 제출 시, 숨겨진 필드 값과 쿠키 값을 모두 서버로 전송합니다.
  4. 일치 여부 확인: 서버는 두 값을 비교하여 일치할 경우에만 요청을 처리합니다.

이 방식의 핵심은 공격자가 쿠키 값을 읽을 수 없다는 점입니다. 동일 출처 정책(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 서명 등)을 함께 사용해야 합니다.

태그: Django CSRF Web Security HttpOnly SameSite

6월 27일 05:07에 게시됨