Search code examples
flaskcsrfcsrf-token

getting CSRF errors from PUT fetch request that does not involve Flask forms


I am getting a CSRF errors in my Flask webapp, from a regular PUT request.
I don't understand why I'm getting these errors, as the PUT requests does not involve any form.

I followed the docs here and initialized my app with CSRF protection like so:

cat app/__init__.py
...
from flask_wtf.csrf import CSRFProtect, generate_csrf, validate_csrf
...
csrf = CSRFProtect()
...
def create_app(config_class=Config):
    app = Flask(__name__)
...
    csrf.init_app(app)
...

the call from javascript is

cat js/js_script1.js
...
        queryUrl = 'https://localhost/put_request1';
        let fetchData = { 
            method: 'PUT'
        };
        let response = await fetch(queryUrl, fetchData);

and I'm seeing errors in the browser, and in the backend - Error: 400 Bad Request: The CSRF token is missing.

The docs mainly talk about CSRF in context of forms, and ajax, but in this case I'm using fetch (is fetch the same as ajax for this matter?) and don't have any form.

In this link in the section: How is the CSRF token constructed there is a code snippet that uses the flask_wtf function generate_csrf to create csrf_token. I tried to implemnet it in app/__init__.py which resulted in other unrelated errors (RuntimeError: Working outside of request context).

In here the csrf_token is read from a 'csrf-token' element in the html page.

const csrfToken = document.querySelector("[name='csrf-token']").content

But I don't have a csrf-token

For comparison, I opened one of my other pages that does have a form and there I do see the csrf_token element as expected:

<input id="csrf_token" name="csrf_token" type="hidden" value="ImI...">

In chrome debugger -> application -> cookies -> https://localhost cookie I only see the session token but not a csrf_token

Questions:

  • Do I need to generate csrf_token when initializing my Flask app?
    (as I understand, Flask does it automatically for FlaskForm, but in this case I don't have a FlaskForm involved)
  • If I do need to generate csrf_token, how can I generate csrf_token for regular POST/PUT fetch requests?

EDIT 1:

Following the reply from @Detlef, I made another attempt.
I tried unsuccessfully to follow the Javascript Requests guidelines in here to get the csrf_token using the following command: var csrf_token = "{{ csrf_token() }}";

I understand that if this code is embedded in an html page then jinja2 substitutes the value that comes back from the function csrf_token() into the variable csrf_token

In my case I make the fetch request from a javascript file (that is included from the html page).
So I don't understand how the command is handled in this context.

I made the following change to the javascript code:

        var csrf_token = "{{ csrf_token() }}";
        console.log('csrf_token', csrf_token); 
        
        let headersData = {
            'Content-Type': 'application/json',
            'X-CSRFToken': csrf_token,
            'X-CSRF-TOKEN': csrf_token,
            'X-CSRF-Token': csrf_token,
        };

        let fetchData = { 
            method: 'PUT',
            headers: headersData
        };

        queryUrl = 'https://localhost/put_request1';
        let response = await fetch(queryUrl, fetchData);

The value of csrf_token variable that shows up in the console, is
csrf_token: {{ csrf_token() }}
which is clearly wrong (nothing gets interpreted).
This then results in another CSRF error in the browser, and in the backend:

Error: 400 Bad Request: The CSRF token is invalid.

EDIT 2:

Per the guidance from @Detlef I:

  • added the meta tag to the .html page
<meta content="{{ csrf_token() }}" name="csrf-token">
  • retrieved the csrf_token in the javascript using:
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");

This solved the problem.
I'm not seeing CSRF errors.
I also checked the failed request in chrome-devtools -> Networks, and I do see in the request header x-csrf-token as expected.

x-csrf-token: ImI0ODY...

Solution

  • The javascript fetch api is supposed to make ajax requests easier.
    So, yes fetch is an ajax request.

    The CSRF token is not generated during the initialization of your flask application. It is more tied to the request. The point is, an attacker could take over someone else's session and send requests on their behalf. As far as I know, it should be a per request token and ensures that the opposing client is still the one who made the previous GET request. Please see this as well.
    There are various discussions about when and how CSRF tokens should be renewed in connection with ajax requests. I am not a security expert and do not want to take part in this at this point.

    Saving the token in an html meta element is I think a sensible approach, which could be used in your case as far as I overlook it. You can also assign the token directly to a javascript variable.
    Just use the csrf_token() function which is mentioned under "JavaScript Requests" in the documentation. The token should then be added to the request headers under "X-CSRFToken".


    It is not possible to use the command csrf_token() outside of a jinja2 template. Pass the token as an argument if possible or use the html meta tag described.

    Add this to your template.

    <meta content="{{ csrf_token() }}" name="csrf-token">
    

    Now use this within your script file.

    const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");