Bare javascript requests will not pass CSRF tests

This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the javascript category.

Last Updated: 2021-05-15

I was given the following JavaScript code to see what files were being downloaded:

const payload = new URLSearchParams({
  name: item.dataset.name,
  original_item_id: item.dataset.originalItemId,
  original_item_type: item.dataset.originalItemType
})

fetch("/track_file_download", {
  method: "POST",
  body: payload
}).then(() => {
  item.innerText = item.innerText + " - Accessed"
  item.classList.add("tracked-download-box")
})

This led to a preponderance of CSRF errors in the backend of the Rails web server.

ActionController::InvalidAuthenticityToken

This happened even though the Rails server supposedly made allowance for JSON requests not having this token:

# This contains a proximate micro-bug
protect_from_forgery with: :exception, unless: -> { request.format.json? }

The proximate, and dumb issue, was that the request format was not actually JSON. It was just JavaScript - and that difference matters. The fix is to add the appropriate Content-Type headers.

  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(data) // body data type must match "Content-Type" header

The bigger picture is that we might want to ensure the CSRF token gets passed with my JavaScript requests. That would obviate the need to disable the protect_from_forgery protections for certain request types.

// the fix
fetch("/track_file_download.json", {
  method: "POST",
  headers: {
    "X-CSRF-Token": document.querySelector("meta[name=csrf-token]").content
  },
  body: payload
}).then(())