pythondjangoajaxtokenverification

Django AJAX: Passing form CSFR verification without proper token attachment. Why?


I have a small Django 5.1.1 project, in which I use an AJAX request to submit form data. I'm using {% csrf_token %} and have the setup for attaching the form csrfToken to the xhr header.

Problem: The CSFR verification is SUCCESSFUL even when xhr.setRequestHeader('X-CSRFToken', csrfToken); is commented out. The AJAX request goes through and gets the response. It only fails when the {% csrf_token %} is missing.

From what I understand it should fail without the form csfrToken attached to the header. What am I missing here?

Simplified code:

<form id="form1" method="post" class="...">
 {% csrf_token %}

 <div>
  <label for="A" class=""></label>
  <input type="text" name="symbol" id="A" class="...">
 </div>

 <div>
   <button type="submit" id="submit-button"></button>
 </div>

</form>

document.getElementById('form1').addEventListener('submit', function(event) {
  event.preventDefault();

  const xhr = new XMLHttpRequest();
  const formData = new FormData(this);
  const csrfToken = this.querySelector('[name=csrfmiddlewaretoken]').value;

  xhr.open('POST', '/form-submit', true);
  // xhr.setRequestHeader('X-CSRFToken', csrfToken);
  xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
  xhr.send(formData);
});
def index_view(request):

    return HttpResponse("", status=200)
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

Solution

  • You are sending the form data, so then the header is not necessary, since the csrf token is in the form data. Indeed, the {% csrf_token %} template tag [Django-doc] translates to a hidden HTML input element, something like:

    <input type="hidden" name="csrfmiddlewaretoken" value="---some-token---">

    There is nothing special about that input element. It is just a HTML element like any other.

    Your JavaScript functions encodes the entire form, so includes the item with the csrfmiddlewaretoken. Django tries to find a CSRF token in the header, and in the POST data, and here it is in the POST data.

    If you would remove the setHeader and the {% csrf_token %}, it will no longer work.

    A lot of times however, AJAX requests do not encode form data, but just data they collect from somewhere else, for example from data-… attributes of HTML elements. Therefore these AJAX requests, that thus don't work with a form that provides a CSRF token, need to manually set the header.