حملهای که از سایت دیگری میآید
فرض کنید کاربر در سایت شما لاگین است و در تب دیگری یک صفحهی مخرب را باز میکند. آن صفحه میتواند یک فرم مخفی بسازد که به /orders/42/cancel/ شما POST شود؛ مرورگر کوکی Session را هم خودکار میفرستد و از دید سرور، خود کاربر درخواست را داده است. این CSRF است. جنگو با CsrfViewMiddleware از همهی درخواستهای «ناامن» (POST، PUT، PATCH، DELETE) یک توکن میخواهد که سایت مهاجم نمیتواند بخواند؛ روی HTTPS هم هدر Origin یا Referer را با دامنهی شما مقایسه میکند.
چرا csrf_exempt راهحل نیست
وقتی AJAX با خطای 403 برمیگردد، سادهترین «راهحل» گذاشتن @csrf_exempt است؛ یعنی دقیقاً همان در را برای مهاجم باز کردن. تنها جای مشروع آن وبهوکهایی است که از سرور دیگری (مثلاً درگاه پرداخت) میآیند و با امضا یا توکن دیگری احراز میشوند.
AJAX درست با fetch
<form id="quote-form" method="post" action="{% url 'orders:quote' %}">
{% csrf_token %}
{{ form.as_div }}
<button type="submit">محاسبهی قیمت</button>
<div id="result"></div>
</form>
const form = document.getElementById("quote-form");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const token = form.querySelector("[name=csrfmiddlewaretoken]").value;
const resp = await fetch(form.action, {
method: "POST",
headers: { "X-CSRFToken": token, "X-Requested-With": "fetch" },
body: new FormData(form),
});
const data = await resp.json();
const box = document.getElementById("result");
box.textContent = data.ok ? `مبلغ: ${data.price_toman} تومان` : "لطفاً خطاهای فرم را اصلاح کنید.";
});
@require_POST
def quote(request):
form = QuoteForm(request.POST)
if form.is_valid():
price = calculate_price(**form.cleaned_data)
return JsonResponse({"ok": True, "price_toman": f"{price // 10:,}"})
return JsonResponse({"ok": False, "errors": form.errors.get_json_data()}, status=400)
form.errors.get_json_data() خطاها را به شکل ساختاریافته (پیام و کد برای هر فیلد) برمیگرداند تا جاوااسکریپت بتواند هر خطا را کنار فیلد خودش نشان دهد.
CSRF پشت nginx و HTTPS
CSRF_TRUSTED_ORIGINS = ["https://example.ir", "https://www.example.ir"] # با scheme
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
نکتههایی که کمتر کسی میداند
- از Django 4.0 مقادیر
CSRF_TRUSTED_ORIGINSباید با scheme باشند (https://)؛ نوشتن فقط دامنه همان 403 معروف «Origin checking failed» را پشت پراکسی میسازد. - اگر صفحه هیچ
{% csrf_token %}ندارد اما جاوااسکریپت آن POST میزند، دکوراتور@ensure_csrf_cookieروی View صفحه کوکی توکن را حتماً ست میکند. - با
CSRF_FAILURE_VIEWمیتوانید بهجای صفحهی انگلیسی پیشفرض، یک صفحهی فارسی بسازید که توضیح دهد «صفحه را رفرش کنید»؛ کاربرانی که فرم را ساعتها باز گذاشتهاند سپاسگزار میشوند. - هر درخواست GET باید بیاثر باشد؛ CSRF فقط از متدهای ناامن محافظت میکند، پس لینک GET که سفارش را لغو کند با هیچ توکنی امن نمیشود.
- در DRF،
SessionAuthenticationفقط برای کاربران لاگینشده CSRF را اجبار میکند؛ پس فرم AJAX مهمان بدون توکن کار میکند اما پس از لاگین ناگهان 403 میدهد.