Does Django process my CSRF token in a PATCH request sent by HTMX?

Hey everyone,
I’d like to ask for clarification in a very special case:
I use Django with HTMX. As per the book “Hypermedia Systems” I try to use the correct request methods for my needs. I have written a mixin that parses PATCH-requests:

class HtmxPatchMixin:
    """
    This Mixin allows class-based views to accept PATCH requests that have been sent by HTMX.
    These requests can contain one or name/value pairs for fields that shall be changed in a given object.
    By using this, I can leverage the capabilities of HTMX and create a kind of "SingleItemManipulationView"
    that accepts GET-Requests to show data from a single object, POST-requests to change the whole object
    (eg with a form) and also PATCH-requests that only change one or a few fields without need for a complete
    form with all fields.
    The CSRF-Token is required for a PATCH-request, so attackers from outside should havve a hard time faking
    these requests. However, no sanity checking or any other checks for unauthorized values are made here, these
    need to be implemented elsewehere, e.g. in the save() method of the model.
    """

    patch_success_url = ""

    def patch(self, request, *args, **kwargs):
        object = self.get_object()

        try:
            # A PATCH request by HTMX will send form-encoded data. This is not expected by Django, therefore no
            # request.POST is created. We need to parse request.body ourselves
            patch_data = parse_qs(request.body.decode("utf-8"))
            # parse_qs returns a list of values. Pick the first one to create a simpler dictionary that we can work
            # with effectively
            patch_data = {field: values[0] for field, values in patch_data.items()}
            print("PATCH data: ", patch_data)
            # Remove the CSRF token. Otherwise, this creates an error because the CSRF token is of course no field in
            # the model
            patch_data.pop("csrfmiddlewaretoken", None)

            for field, value in patch_data.items():
                if hasattr(object, field):
                    setattr(object, field, value)
                else:
                    return HttpResponseBadRequest(f"Invalid field: {field}")

            # Save the object after updating fields
            object.save()

        except Exception as e:
            return HttpResponseBadRequest(f"Error processing request: {str(e)}")

        # After updating the given fields we want to redirect to the success_url, but if we only
        # redirect here, HTMX would send a PATCH request (the same type as the original request)
        # to the redirect address. We want it to send a GET request, so we have to set the correct
        # status code and HX-Redirect header.
        response = HttpResponse(status=303)
        if self.patch_success_url:
            response.headers["HX-Redirect"] = self.patch_success_url
        else:
            response.headers["HX-Redirect"] = self.get_template_names()
        return response

Here is a snippet that shows the template sending the request:

<div class="col col-2 align-content-center text-center">
                    <form>
                        {% csrf_token %}
                        <button type="button"
                                class="btn btn-success"
                                hx-patch="{% url "tasks:task" task.pk %}"
                                hx-vals='{"status":"{{ status_choices.COMPLETED }}"}'
                                hx-target="#main_content">
                            Aufgabe erledigt
                        </button>
                    </form>
                </div>

Now, my question is if the CSRFViewMiddleware checks the CSRF token in this case. In my understanding, if it worked, it would remove the CSRF token from the form-encoded content before giving it to the view? Do I need to check for the validity of the token by hand here?

Thanks for any help!

André

I think I just found the answer myself:
I included this in base.html:

<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>

So it seems, that in this case the CSRF-token was included twice in the request (once in the header and once in the form data) which caused the confusion. I tested this and when I remove both metions of the CSRF-token, Django does not process the request, instead giving me an error in the terminal:

Forbidden (CSRF token missing.): /tasks/4/
[06/Jan/2025 10:38:20] "PATCH /tasks/4/ HTTP/1.1" 403 2681

When I only include the CSRF-token in the form, I still get the error. When I only onclude it with the header (via the body-tag), everything works fine and I don’t have to remove the CSRF-token from the form-data anymore.

My conclusion: The CSRF-token is included via the body-tag in the base.html and is properly checked by the middelware. Can somebody confirm this?

The CsrfViewMiddleware will only look for the csrfmiddlewaretoken value if request.method == "POST"

This was raised as an issue at #35062 (Update csrf.py to check request.POST if request.method is not GET) – Django.

I also want to point out that how you’re processing your input is very insecure. You really want to process this data using a form, with the same logic and processing as if the data were submitted using POST instead of PATCH. In fact, if this is a mixin for the standard Django CBVs, I could see implementing this as the patch method:

def patch(self, request, *args, **kwargs):
    return self.post(request, *args, **kwargs)

(With the possibility that an alternate form may need to be used for setting all fields as optional in the form based upon your description.)

OK… So, this means that the CSRF-token is not checked when I send a PATCH-request? But why do I then get an error if I leave it out? I’m really asking for my understanding here, since I am pretty lost. Everything besides GET and POST requests seems to be never mentioned anywhere.

And what do you mean by “Process this data using a form”? As far as I see, the data is sent as a form, HTMX only adds a value to an otherwise empty form?

I tried self.post, but it doesn’t seem to be created when using PATCH-requests…?

Which solution would you suggest here? My goal is to only change one field of an existing object. In this particular case, with a click on a button, a task should be marked as “completed”. In the save() method of the model, a timestamp is then added. The options I thopught about were:

  • Ignore the request type and use a POST-request. But then, how do I only change 1 value? You said to create a separate form containing only one field. But how can I then get the data from the form into the object? Or should I create hidden form fields for all the other values? Seems like a lot of unnecessary data would be transferred that way. Would this be safe or could these be changed by the user just by poking around in the browser dev-tools?
  • Change the button to type="submit". Is that what you meant with sending a form? Would this lead to a request.post in the view?

Greetings
André

I have tried this now:

The template snippet:

<form>
    <input type="hidden" name="status" value="{{ status_choices.COMPLETED }}">
        <button type="submit"
                class="btn btn-success"
                hx-patch="{% url "tasks:task" task.pk %}"
                hx-target="#main_content"
        >
            Aufgabe erledigt
        </button>

The Mixin:

class HtmxPatchMixin:
    patch_success_url: str = ""

    def patch(self, request, *args, **kwargs):
        print("POST DATA: ", self.request.POST)
        return self.post(request, *args, **kwargs)

The Output of the print() statement:

POST DATA:  <QueryDict: {}>

So, I guess, really no request.POST is created when using PATCH-requests.

I also changed the form’s method to POST, which gave me a request.POST, but also a
“ValueError: The view tasks.views.view didn’t return an HttpResponse object. It returned None instead.” I guess, that is because I did not sent a form with all the fields.

Would it really be a better and more secure solution here to create a form that has all required fields in it? And why is my parsing of the PATCH-data from the initial post insecure?

Greetings
Andre

And… Another round.

I’ve read it up in the documentation.
Source:

“For all incoming requests that are not using HTTP GET, HEAD, OPTIONS or TRACE, a CSRF cookie must be present, and the ‘csrfmiddlewaretoken’ field must be present and correct. If it isn’t, the user will get a 403 error.”

To me, this means that PATCH-requests are protected. Maybe they changed it during the last year?

Not quite. The token is checked for any “unsafe” request. (RFC 9110 - HTTP Semantics). However, Django will only get the token from the post data if the request is a POST. Using the PUT, PATCH, or DELETE verbs requires that the token be submitted in the X-CSRFToken header. (That header name itself is configurable, see CSRF_HEADER_NAME).

See the docs at How it works for more details on this.

This matches the behavior you are seeing. When you include the header, it works. Including or removing the {% csrf_token %} is immaterial, since the middleware won’t look for it on anything other than a POST.

Correct - GET, HEAD and POST are the only verbs issued by a browser. JavaScript must be used to issue requests with any other verbs.

Potential confusion of terminology here. I’m referring to using a Django form in the view, where you bind the submitted data. (I’m not referring to the HTML form in the browser.)
Process the PATCH submission in exactly the same way you would handle a POST.
eg: patch_form = MyModelForm(self.request.POST, instance=self.object)

self.post is a method in a CBV, not the data being submitted. The request object is bound in the CBV to self.request, and so the POST data is accessible as self.request.POST.

The get_form method in the CBVs handles all this.

You might find it worthwhile to look at UpdateView -- Classy CBV to get a better understanding of everything happening in the UpdateView CBV.

What I suggested above - create a Django form for the object. If you know you’re only updating one field, then create the form with only that field.
Bind the object being updated as the instance in the form, along with the data being submitted. You don’t need to send this form as rendered html as part of the GET. Use it in your PATCH handling only.

Rendering HTML is only one of the functions of a Django form. You can use forms without ever having rendered them.

I’d first verify that the browser is actually sending what you think it’s sending. Check the network tab of your browser’s developer tools to verify that the data is what you think it’s supposed to be.

“A view is a function that accepts a request and returns a response.”

What are you returning from your patch function?

Let’s clarify what you’re referring to here - are you talking about your HTML form or a Django form? They are two different things.

Because you are not performing any validation on the submitted data. In fact, you’re only checking to see if the fields being returned are fields in the model. That means that someone using your app could modify the data being returned to change fields that you don’t want changed - or change those fields to undesired values. It’s that validation that a Django form can provide you.

However, I’d have another take on this. If the only purpose of this html form is to change a single field to a specific value, I wouldn’t submit any data at all. I’d create a view for that purpose and have this button submit to that view.

(Note: Regardless of the specific mechanism being used, you might also want to ensure that the person updating the data is authorized to update that particular instance. You may want to ensure that person “A” doesn’t change the url being submitted to, to ensure that they’re not changing something that should only be changed by person “B”.)

Never implicitly trust anything submitted from the browser.

OK. First of all, thanks a lot for the explanation - The part with the validation of the submitted data makes a lot of sense and I should definitely use a form.

I followed what you said and tried it again… But without success. For simplicity, I skipped all the Mixin Voodo and wrote a new class-based view. It uses the TaskForm and spoecifies the field to use. I also tried using a separate form that only contained the status field and use that - same effect.

class TaskCompletedView(UpdateView):
    model = Task
    form = TaskForm
    fields = ["status"]

    def patch(self, request, *args, **kwargs):
        """Handle PATCH requests for partial object updates"""
        print("Request data: ", self.request.body)
        self.post(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        print("POST DATA: ", self.request.POST)
        super().post(request, *args, **kwargs)

    def get_success_url(self):
        return reverse_lazy("tasks:user_task_list")

Here is the snippet from the template:

<form>
    {% csrf_token %}
    <input type="hidden" name="status", value="{{ status_choices.COMPLETED }}">
    <button type="submit"
            class="btn btn-success"
            hx-patch="{% url "tasks:task_completed" task.pk %}"
            hx-target="#main_content">
        Aufgabe erledigt
    </button>
</form>

The PATCH-request is sent correctly and the body contains the data:

I played around with printing request data out of curiosity.
self.request.POST is empty. The print statement gives me:

<QueryDict: {}>

print("Request data: ", self.request.body) gave this result:

Request data:  b'csrfmiddlewaretoken=7Tro7DdcM57eX8CdyQldQb9F2wACw33QFQo3Hq7UaqFRQifQFzLaN5wBpp0JPioC&status=CO'

Obviously, the data is sent in the body of the request, but no self.request. POST is generated.
Can I somehow do this in the patch() method and then call post() to process the form?

Regarding your question to clarify the implied meaning of “form”: In that case, I meant writing out a HTML form that has a bunch of hidden fields in it - Which doesn’t make a lot of sense to me since here, the user could manipulate all the values in the browser dev tools.

Currently, I am thinking about your other proposed solution - To just create a view that receives an empty request and then sets the task to “completed”. However, this would not be very future-proof… I was hoping for a class-based view that could securely update any field from a given form.

Greetings
André

That’s interesting - I don’t think I’ve ever realized this before. (In the few cases in the past where I’ve used non-browser verbs (other than GET or POST), it has always been to submit JSON, so I don’t believe I’ve ever even looked at this.

Fundamentally, it should be easy:

from django.http.request import QueryDict
request.POST = QueryDict(self.request.body)

Agreed - which is why I was trying to clarify this point. I don’t see a need for an HTML form for this at all.

Why would it not be? You’ve got a number of different ways to add functionality to this concept.

Except I find it hard to believe that you would want to update (literally) any field. At a minimum, I would assume that you would not want the id field changed.

Again, you can not trust anything coming from the browser. The more you limit what the browser can send, the safer you’re going to be.

As an example of what might be considered the “ideal world” to prevent tampering, you wouldn’t even send the pk of the row being modified. If the sequence of events is sufficiently well-defined, you keep the pk in the user’s session on the server - and that is the object being acted upon.