CSRF(Cross-Site Request Forgery)는 인증된 사용자의 권한을 이용해 의도하지 않은 요청을 서버에 강제로 수행하게 만드는 공격이다. 즉, 사용자가 원치 않는데도 어떤 요청이 자동으로 실행되는 상황으로 이해할 수 있다.
<img src="https://yourbank.com/transfer?amount=100000&to=hacker" />
→ 사용자가 로그인된 상태에서 위와 같은 공격자의 페이지에 접속하면, 쿠키가 자동 전송되면서 서버는 이를 정상 요청으로 처리하게 된다.
CORS(Cross-Origin Resource Sharing)는 서로 다른 출처(origin) 간의 요청을 브라우저가 허용하도록 만드는 보안 정책이다. JavaScript에서 외부 API를 호출할 때 흔히 마주치는 이슈이다.
http://localhost:3000
http://localhost:8000
→ 도메인, 포트, 프로토콜 중 하나라도 다르면 브라우저가 요청을 차단한다.
django-cors-headers
라이브러리 설치Access-Control-Allow-Origin
, Access-Control-Allow-Credentials
헤더 설정항목 | CSRF | CORS |
---|---|---|
대상 | 사용자의 인증된 세션 | 다른 출처에서 온 JS 요청 |
발생 위치 | 서버 | 브라우저 |
방지 목적 | 악의적 요청 차단 | 출처 제한 |
Django 대응 | {% csrf_token %} / X-CSRFToken | django-cors-headers 설정 |
⚠️ 참고: 본 서비스는 Django가 HTML, 정적 파일, API를 모두 처리하는 단일 백엔드 구조이므로, CORS 설정은 불필요하다.
<img src="https://bank.example.com/withdraw?amount=100000" />
→ 로그인된 사용자가 외부 사이트를 통해 원치 않는 요청을 하게 되는 상황이다.
fetch("http://localhost:8000/api", {
method: "GET",
credentials: "include"
});
→ 프론트엔드와 백엔드가 다른 출처에 있을 경우, 브라우저가 요청을 차단한다. 이 경우 백엔드에서 CORS 허용 설정이 필요하다.
django-cors-headers
적용법pip install django-cors-headers
INSTALLED_APPS = ['corsheaders', ...]
MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware', ...]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"https://yourfrontend.com"
]
저장 위치 | CSRF 방지 | 설명 |
---|---|---|
localStorage | O | JS에서 직접 헤더에 넣기 때문에 CSRF 방지 가능 (단, XSS 위험 높음) |
HttpOnly 쿠키 | X | 자동 전송되기 때문에 CSRF에 취약하다 |
✅ 정리: JWT를
HttpOnly 쿠키
에 저장하는 경우, 별도의 CSRF 방어 로직이 반드시 필요하다.
항목 | 세션 인증 | JWT 인증 |
---|---|---|
저장 위치 | 서버 세션 | 클라이언트 쿠키/스토리지 |
확장성 | 낮음 | 높음 |
CSRF 대응 | 필요 (쿠키 기반) | 필요 (HttpOnly 쿠키일 경우) |
재발급 | 서버에서 관리 | Refresh Flow 직접 구현 필요 |
옵션 | 설명 | 보안 목적 |
---|---|---|
Secure | HTTPS에서만 쿠키 전송 | 네트워크 도청 방지 |
HttpOnly | JS 접근 불가 | XSS 방지 |
SameSite | 타 도메인 요청 제한 | CSRF 방지 |
SameSite 옵션:
Strict
: 완전 차단 (가장 강력함)Lax
: 대부분의 GET 요청 허용None
: 외부 요청 허용 (Secure 옵션 필수)from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def webhook(request):
...
{% csrf_token %}
을 통한 CSRF 방어<form method="POST" action="/submit/">
{% csrf_token %}
<input name="email">
</form>
headers: {
"X-CSRFToken": getCookie("csrftoken")
}
get_token()
또는 {% csrf_token %}
으로 발급한다.X-CSRFToken
헤더로 토큰을 전송한다.HttpOnly
, Secure
, SameSite=Strict
설정된 쿠키에 저장한다.csrftoken
)에 저장하며 JS에서 읽을 수 있도록 한다.X-CSRFToken
헤더에 포함시켜야 한다.회원가입은 대부분 인증이 필요 없는 공개 요청이지만, CSRF 공격을 방지하기 위해 반드시 CSRF 토큰이 포함되어야 한다. 서버가 렌더링한 템플릿에 {% csrf_token %}
을 포함하면, 이 토큰이 csrftoken
쿠키로 전달된다. 이후 JavaScript에서 이 토큰을 읽어 AJAX 요청 시 헤더로 함께 전송해야 한다.
<!-- 서버에서 렌더링되는 HTML 템플릿 -->
<form id="signup-form">
{% csrf_token %}
<input type="text" name="email" placeholder="이메일">
<input type="password" name="password" placeholder="비밀번호">
<button type="submit">가입</button>
</form>
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
$("#signup-form").on("submit", function(e) {
e.preventDefault();
const email = $("input[name='email']").val();
const password = $("input[name='password']").val();
$.ajax({
type: "POST",
url: "/register/",
headers: { "X-CSRFToken": getCookie("csrftoken") },
data: { email, password },
success: function(response) {
alert("회원가입 성공!");
},
error: function(error) {
alert("오류 발생");
}
});
});
로그인 요청 시에는 서버가 사용자의 자격을 확인하고 JWT를 발급한다. 이 JWT는 HttpOnly 쿠키에 저장되며, 클라이언트가 직접 접근할 수 없다. 동시에 서버는 CSRF 토큰도 함께 쿠키로 내려보낸다. 이 설정이 완료되면 이후의 모든 상태 변경 요청에서 CSRF 검증을 수행할 수 있다.
from django.middleware.csrf import get_token
response = JsonResponse({"message": "로그인 성공"})
response.set_cookie("access_token", jwt_token, httponly=True, secure=True, samesite='Strict')
response.set_cookie("csrftoken", get_token(request), httponly=False, secure=True, samesite='Strict')
return response
로그인 이후에는 사용자가 페이지를 이동하거나 AJAX 요청을 보낼 수 있다. 이때 JWT는 자동으로 쿠키에 포함되어 전송된다. 다만, CSRF 방지를 위해 JavaScript에서는 쿠키에서 csrftoken
을 읽어 요청 헤더에 포함시켜야 한다.
$.ajaxSetup({
beforeSend: function(xhr) {
xhr.setRequestHeader("X-CSRFToken", getCookie("csrftoken"));
}
});
GET 요청은 리소스를 조회하는 용도이기 때문에 일반적으로 상태 변경을 일으키지 않는다. 따라서 CSRF 검증은 필요하지 않다. 다만, 인증이 필요한 경우 JWT 쿠키가 자동으로 포함되어야 한다.
fetch("/api/profile", {
method: "GET",
credentials: "include"
});
POST 요청은 데이터를 생성하거나 수정하는 요청이기 때문에 반드시 CSRF 토큰이 필요하다. 템플릿 기반이라면 {% csrf_token %}
을 form 내부에 포함하고, JavaScript 기반 요청에서는 쿠키에서 csrftoken
을 읽어 X-CSRFToken
헤더로 함께 전송해야 한다.
<form method="POST" action="/submit/">
{% csrf_token %}
<input name="bio">
</form>
fetch("/submit/", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": getCookie("csrftoken")
},
body: JSON.stringify({ bio: "소개글입니다" })
});