فصل ۵: فرم‌ها و اعتبارسنجی — ورودی امن و فارسی

CSRF و فرم‌های AJAX: چرا هرگز csrf_exempt

حمله‌ای که از سایت دیگری می‌آید

فرض کنید کاربر در سایت شما لاگین است و در تب دیگری یک صفحه‌ی مخرب را باز می‌کند. آن صفحه می‌تواند یک فرم مخفی بسازد که به /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 می‌دهد.

برای ذخیره‌ی پیشرفت و شرکت در آزمون، وارد شوید — رایگان است.