34. Practical Exercise: Version 14
The [http-servers/09] folder for version 14 is obtained by copying the [http-servers/08] folder from version 13.
34.1. Introduction
CSRF (Cross-Site Request Forgery) is a session hijacking technique. It is explained as follows on Wikipedia (https://fr.wikipedia.org/wiki/Cross-site_request_forgery):
- Malorie manages to find out the link that allows her to delete the message in question.
- Malorie sends a message to Alice containing a pseudo-image to display (which is actually a script). The image’s URL is the link to the script that deletes the desired message.
- Alice must have an open session in her browser for the site Malorie is targeting. This is a prerequisite for the attack to succeed silently without triggering an authentication request that would alert Alice. This session must have the necessary permissions to execute Malorie’s destructive request. It is not necessary for a browser tab to be open on the target site, nor even for the browser to be running. It is sufficient for the session to be active.
- Alice reads Malorie’s message; her browser uses Alice’s open session and does not request interactive authentication. It attempts to retrieve the image’s content. In doing so, the browser triggers the link and deletes the message, retrieving a text-based web page as the image’s content. Since it doesn’t recognize the associated image type, it doesn’t display an image, and Alice doesn’t realize that Malorie has just made her delete a message against her will.
Even explained this way, the CSRF technique is difficult to understand. Let’s draw a diagram:

- In [1-2], Alice communicates with the forum (Site A). This forum maintains a session for each user. Alice’s browser stores this session cookie locally and sends it back every time it makes a new request to Site A;
- In [3], Malorie sends a message to Alice. Alice reads it in her browser. The message is in HTML format and contains a link to an image on Site B. In fact, this link is a link to a JavaScript script that runs once it reaches Alice’s browser;
- This JavaScript script then makes a request to Site A. Alice’s browser automatically sends the request along with the locally stored session cookie. This is where the attack occurs: Malorie has successfully accessed Site A using Alice’s session credentials. From this point on, regardless of what happens, the attack has taken place;
To counter this type of attack, Site A can proceed as follows:
- With each exchange [1-2] with Alice, Site A sends a key, hereafter referred to as a CSRF token, which Alice must return in her next request. Thus, with each request, Alice must send two pieces of information:
- the session cookie;
- the CSRF token received in the response to her last request to Site A;
This is where the protection lies: while the browser automatically sends the session cookie back to Site A, it does not do so for the CSRF token. For this reason, exchange 6-7 performed by the attack script will be rejected because request 6 will not have sent the CSRF token;
Site A can send Alice the CSRF token in various ways for an HTML application:
- It can send an HTML page with every request where all links contain the CSRF token, for example [http://siteA/chemin/csrf_token]. When Alice clicks on one of these links during the next request, Site A will simply retrieve the CSRF token from the request URL and verify that it is valid. This is what will be done here;
- for HTML pages containing a form, it can send the form with a hidden field [input type='hidden'] containing the CSRF token. This will then be automatically submitted with the form when Alice submits the page. Site A will retrieve the CSRF token from the body of the request;
- other techniques are possible;
34.2. Configuration

We add two booleans to the application’s [parameters] configuration:
- [with_redissession]: When set to True, the application uses a Redis session. When set to False, the application uses a standard Flask session;
- [with_csrftoken]: When set to True, the application’s URLs contain a CSRF token;
# durée pause thread en secondes
"sleep_time": 0,
# serveur Redis
"with_redissession": True,
"redis": {
"host": "127.0.0.1",
"port": 6379
},
# token csrf
"with_csrftoken": False,
34.3. CSRF Implementation
We will ensure that when:
config['parameters']['with_csrftoken']
is set to [True], the application sends web pages to the client browser whose links will contain a CSRF token.
34.3.1. The [flask_wtf] module
The CSRF token will be implemented using the [flask_wtf] module, which we install in a PyCharm terminal:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install flask_wtf
Collecting flask_wtf
…
34.3.2. View templates
We are introducing a new class in the models:

The [AbstractBaseModelForView] class is as follows:
- line 9: the [AbstractBaseModelForView] class implements the [InterfaceModelForView] interface implemented by the model classes;
- lines 11–13: the [get_model_for_view] method is not implemented;
- Lines 15–20: The [get_csrftoken] method generates the CSRF token if the application has been configured to use them. Depending on the situation, the function returns a token preceded by a slash (/) or an empty string. The [generate_csrf] function always generates the same value for a given client request. Processing a request involves executing various functions. Using [generate_csrf] in these functions always generates the same value. On the next request, however, a new CSRF token is generated;
All M models for view V will include the CSRF token as follows:
- Each model class extends the base class [AbstractBaseModelForView];
- Line 8: The CSRF token is requested from the parent class. We get either an empty string or a string like [/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c];
34.3.3. The Views
From what we’ve just seen, all views V will have the CSRF token in their template M. They can therefore use it in the links they contain. Let’s look at a few examples:
The authentication fragment [v_authentification.html]
<!-- form HTML - post its values with the [authenticate-user] action -->
<form method="post" action="/authentifier-utilisateur{{modèle.csrf_token}}">
<!-- title -->
<div class="alert alert-primary" role="alert">
<h4>Veuillez vous authentifier</h4>
</div>
…
</form>
- line 2: based on what we just saw, the URL for the [action] attribute will be:
[/authentifier-utilisateur/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c]
or
[/authentifier-utilisateur]
depending on whether the application has been configured to use CSRF tokens;
The tax calculation fragment [v-calcul-impot.html]
<!-- form HTML posted -->
<form method="post" action="/calculer-impot{{modèle.csrf_token}}">
<!-- 12-column message on blue background -->
<div class="col-md-12">
<div class="alert alert-primary" role="alert">
<h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
</div>
</div>
…
</form>
The simulations section [v-liste-simulations.html]
{% if modèle.simulations is undefined or modèle.simulations|length==0 %}
<!-- message on blue background -->
<div class="alert alert-primary" role="alert">
<h4>Votre liste de simulations est vide</h4>
</div>
{% endif %}
{% if modèle.simulations is defined and modèle.simulations|length!=0 %}
<!-- message on blue background -->
<div class="alert alert-primary" role="alert">
<h4>Liste de vos simulations</h4>
</div>
<!-- simulation table -->
<table class="table table-sm table-hover table-striped">
…
<!-- table body (data displayed) -->
<tbody>
<!-- display each simulation by browsing the simulation table -->
{% for simulation in modèle.simulations %}
<!-- display a table row with 6 columns - <tr> tag -->
<!-- column 1: row header (simulation no.) - <th scope='row' tag -->
<!-- column 2: parameter value [married] - <td> tag -->
<!-- column 3: parameter value [children] - <td> tag -->
<!-- column 4: parameter value [salary] - <td> tag -->
<!-- column 5: [tax] parameter value - <td> tag -->
<!-- column 6: parameter value [surcôte] - <td> tag -->
<!-- column 7: parameter value [discount] - <td> tag -->
<!-- column 8: parameter value [reduction] - <td> tag -->
<!-- column 9: parameter value [rate] (of tax) - <td> tag -->
<!-- column 10: link to delete simulation - <td> tag -->
<tr>
<th scope="row">{{simulation.id}}</th>
<td>{{simulation.marié}}</td>
<td>{{simulation.enfants}}</td>
<td>{{simulation.salaire}}</td>
<td>{{simulation.impôt}}</td>
<td>{{simulation.surcôte}}</td>
<td>{{simulation.décôte}}</td>
<td>{{simulation.réduction}}</td>
<td>{{simulation.taux}}</td>
<td><a href="/supprimer-simulation/{{simulation.id}}{{modèle.csrf_token}}">Supprimer</a></td>
</tr>
{% endfor %}
</tr>
</tbody>
</table>
{% endif %}
The menu snippet [v-menu.html]
<!-- bootstrap menu -->
<nav class="nav flex-column">
<!-- display a list of links HTML -->
{% for optionMenu in modèle.optionsMenu %}
<a class="nav-link" href="{{optionMenu.url}}{{modèle.csrf_token}}">{{optionMenu.text}}</a>
{% endfor %}
</nav>
34.3.4. Routes
There are now two types of routes, depending on whether or not they use a CSRF token:

- [routes_without_csrftoken] are routes without a CSRF token. These are the routes from the previous version;
- [routes_with_csrftoken] are routes with a CSRF token.
In [routes_with_csrftoken], routes now have an additional parameter, the CSRF token:
All routes now have the CSRF token in their parameters, including the [/init-session] route. This means that the client cannot launch the application by directly typing the URL [/init-session/html] because the CSRF token will be missing. It must now go through the [/] URL in lines 7–10.
The routes are selected in the main script [main]:
- lines 9–13: selecting routes depending on whether the application uses CSRF tokens;
34.3.5. The [MainController]
For each request, the server must verify the presence of the CSRF token. We will do this in the main controller [MainController], which handles all requests:
- Line 20: Retrieve the CSRF token from the request URL of the form [http://machine:port/path/action/param1/param2/…/csrf_token]. The session token is always the last element of the URL;
- line 23: the validity of the CSRF token retrieved from the URL is checked against the session’s CSRF token. If it is invalid, the [validate_csrf] function throws a [ValidationError] exception (line 27);
- line 41: the CSRF token is included in the response sent to the client. JSON and XML clients will need it. This is because these clients do not receive HTML pages with the CSRF token in the links contained within the pages. They will therefore receive it in the JSON or XML response sent by the server;
Note: The [validate_csrf] function on line 23 does not check for an exact match. The CSRF token is stored in the session under the key [csrf_token]. Tests seem to indicate that a CSRF token is valid if it was generated during the session. Thus, if you manually replace the CSRF token [xyz] in the URL displayed in the browser—for example, (/lister-simulations/xyz)—with another token [abc] previously received during a prior action, the [/lister-simulations] action will succeed;
34.4. Tests with a browser
First:
- start the server with the [with_csrftoken] parameter set to [True];
- request the URL [http://localhost:5000] using a browser;

- in [1], the CSRF token;
Let’s perform some operations until we have a list of simulations:

Now, manually enter the URL [http://localhost:5000/supprimer-simulation/1/x] to delete the simulation with id=1. We intentionally enter an incorrect CSRF token to see what happens. The server’s response is as follows:

Note 1: It is not certain that the method used here is always sufficient to counter CSRF attacks. Let’s return to the attack diagram:

If the JavaScript script downloaded in [5] is capable of reading the browser history used by Alice, it will be able to retrieve the URLs executed by the browser, such as [/target/csrf_token]. It can then retrieve the session token [csrf_token] and carry out its attack in [6-7]. However, the browser only allows access to the history of the browser window in which the script is running. Therefore, if Alice does not use the same window to interact with Site A [1-2] and read Malorie’s message [3], the CSRF attack will not be possible.
34.5. Console clients
Another way to test version 14 of the application is to reuse the tests from version 12 and adapt them to the new server.

The [impots/http-clients/09] folder is initially created by copying the [impots/http-clients/07] folder. It is then modified.
Let’s return to the routes that initialize a session:
None of these routes are suitable for initializing a JSON or XML session:
- lines 2–5: the [/] route initializes an HTML session;
- lines 8–11: the [/init-session] route requires a CSRF token that we don’t know;
We decide to add a new route to the server:
- line 2: the new route. It does not expect a CSRF token. We have thus returned to the [/init-session] route from the previous version;
- lines 4-5: we redirect the client (JSON, XML, HTML) to the [/init-session] route, which includes the CSRF token in its parameters;
You can test this new route in a browser:

The server’s response (configured with [with_csrftoken=True]) is as follows:

- in [1], the server was redirected to the [/init-session] route with the CSRF token in the URL;
- in [2], the CSRF token is in the JSON dictionary sent by the server, associated with the [csrf_token] key;
Let’s go back to the client code:

We modify the [config] configuration as follows:
config.update({
# fichier des contribuables
"taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
# fichier des résultats
"resultsFilename": f"{script_dir}/../data/output/résultats.json",
# fichier des erreurs
"errorsFilename": f"{script_dir}/../data/output/errors.txt",
# fichier de logs
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# le serveur de calcul de l'impôt
"server": {
"urlServer": "http://127.0.0.1:5000",
"user": {
"login": "admin",
"password": "admin"
},
"url_services": {
"calculate-tax": "/calculer-impot",
"get-admindata": "/get-admindata",
"calculate-tax-in-bulk-mode": "/calculer-impots",
"init-session": "/init-session-without-csrftoken",
"end-session": "/fin-session",
"authenticate-user": "/authentifier-utilisateur",
"get-simulations": "/lister-simulations",
"delete-simulation": "/supprimer-simulation",
}
},
# mode debug
"debug": True,
# csrf_token
"with_csrftoken": True,
}
)
…
# route init-session
url_services = config['server']['url_services']
if config['with_csrftoken']:
url_services['init-session'] = '/init-session-without-csrftoken'
else:
url_services['init-session'] = '/init-session'
- line 31: a boolean will indicate to the client whether the server it is addressing works with CSRF tokens or not;
- lines 37–40: the service URL for the [init-session] action is set:
- if the server uses CSRF tokens, then the service URL is [/init-session-without-csrftoken];
- otherwise, the service URL is [/init-session];
The route [/init-session-without-csrftoken] has been introduced. It allows a JSON/XML client to start a session with the server without having a CSRF token. The client will find this token in the server’s response.
We then modify the [ImpôtsDaoWithHttpSession] class implementing the client’s [dao] layer:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | |
- lines 38–92: CSRF token handling occurs primarily within the [get_response] method;
- line 60: the key point is the [allow_redirects=True] parameter. This is its default value, but we wanted to highlight it;
When in [with_csrftoken=True] mode:
- clients begin their interaction with the server by calling the route [/init-session_without_csftoken/type_response];
- the server responds to this request with a redirect to the route [/init-session/type_response/csrf_token];
- Because of the [allow_redirects=True] parameter, this redirect will be followed by the client [requests];
- the CSRF token will be found in the retrieved result on lines 72 and 74 associated with the key [csrf_token];
When in [with_csrftoken=False] mode:
- (continued)
- clients begin their interaction with the server by calling the route [/init-session/type_response];
- the server responds to this request with a redirect to the route [/init-session/type_response];
- because of the [allow_redirects=True] parameter, this redirect will be followed by the client [requests];
- there is no CSRF token to retrieve on lines 81–82. The property [self.__csrf_token] therefore remains None (line 36);
- lines 51–52: for all subsequent requests, the CSRF token, if it exists, is added to the initial route;
- lines 81–82: the new token generated by the server for each new client request is stored locally to be returned on line 52 with the next request;
Additionally, the [init_session] method changes slightly:
It’s important to remember here that we created a route [/init-session-without-csrftoken/<response-type>] to initialize the client/server dialogue without a CSRF token. However, we’ve seen that the [get_response] method called on line 12 of the code systematically appends the CSRF token stored in [self.__csrf_token] to the end of the service URL. That is why, on line 6 of the code, we remove this CSRF token if it exists.
That’s it. For testing, we’ll run:
- the console clients [main, main2, main3];
- the test classes [Test1HttpClientDaoWithSession] and [Test2HttpClientDaoWithSession];
by successively setting the configuration parameter [with_csrftoken] to True and then False.

Here is an example of the logs obtained when running the [main json] client with [with_csrftoken=True]:
2020-08-08 16:33:23.317903, MainThread : début du calcul de l'impôt des contribuables
2020-08-08 16:33:23.317903, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-08-08 16:33:23.317903, Thread-2 : début du calcul de l'impôt des 2 contribuables
2020-08-08 16:33:23.317903, Thread-3 : début du calcul de l'impôt des 4 contribuables
2020-08-08 16:33:23.317903, Thread-4 : début du calcul de l'impôt des 1 contribuables
2020-08-08 16:33:23.379221, Thread-2 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"], "csrf_token": "ImFiZmZkYjZmMzFkZDc2YWRjNWYwOGM0NTBmMGM4ODJjYzViOWI4NGEi.Xy63sw.H5L0--yWsvfaWvggrGw78z5VnN0"}
2020-08-08 16:33:23.381073, Thread-4 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"], "csrf_token": "ImY5YzQyMjlkYzcyYmM4YmZiMGI0NWY5MjE4MzIzNDExZjc0MGQ3MWQi.Xy63sw.q6olg7IP_g2ro_RBFRCX1BX90g8"}
2020-08-08 16:33:23.386982, Thread-3 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"], "csrf_token": "IjkxZGNlN2YyMmUxMjQ0M2Y0MTdjNDQ4ZmQ1MDMxZjkwNjBhNzAzZjMi.Xy63sw.-6buL11No3UJBlElpW4tX4B-lp0"}
2020-08-08 16:33:23.390269, Thread-1 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"], "csrf_token": "IjIxNmU4MDQyZDFmZmIyZDlmZjE4MzNlNDUzYzFjMGYxMWYxYzEwNGYi.Xy63sw.fgs6Cm2owsJf4NjTm7gKrVESabI"}
2020-08-08 16:33:23.413206, Thread-2 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie", "csrf_token": "ImFiZmZkYjZmMzFkZDc2YWRjNWYwOGM0NTBmMGM4ODJjYzViOWI4NGEi.Xy63sw.H5L0--yWsvfaWvggrGw78z5VnN0"}
2020-08-08 16:33:23.422877, Thread-2 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 2}], "csrf_token": "ImFiZmZkYjZmMzFkZDc2YWRjNWYwOGM0NTBmMGM4ODJjYzViOWI4NGEi.Xy63sw.H5L0--yWsvfaWvggrGw78z5VnN0"}
2020-08-08 16:33:23.428622, Thread-4 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie", "csrf_token": "ImY5YzQyMjlkYzcyYmM4YmZiMGI0NWY5MjE4MzIzNDExZjc0MGQ3MWQi.Xy63sw.q6olg7IP_g2ro_RBFRCX1BX90g8"}
2020-08-08 16:33:23.429127, Thread-3 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie", "csrf_token": "IjkxZGNlN2YyMmUxMjQ0M2Y0MTdjNDQ4ZmQ1MDMxZjkwNjBhNzAzZjMi.Xy63sw.-6buL11No3UJBlElpW4tX4B-lp0"}
2020-08-08 16:33:23.429127, Thread-1 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie", "csrf_token": "IjIxNmU4MDQyZDFmZmIyZDlmZjE4MzNlNDUzYzFjMGYxMWYxYzEwNGYi.Xy63sw.fgs6Cm2owsJf4NjTm7gKrVESabI"}
2020-08-08 16:33:23.429127, Thread-2 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée", "csrf_token": "IjU1YjlmZDA0OWRhNTJlODFmYjgyYjlhM2ExYWNhZmUzNTk2NjA5NGIi.Xy63sw.nyNSvkcG6iG0oIMBjtYPo8ySgdw"}
2020-08-08 16:33:23.438519, Thread-2 : fin du calcul de l'impôt des 2 contribuables
2020-08-08 16:33:23.443033, Thread-4 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 1}], "csrf_token": "ImY5YzQyMjlkYzcyYmM4YmZiMGI0NWY5MjE4MzIzNDExZjc0MGQ3MWQi.Xy63sw.q6olg7IP_g2ro_RBFRCX1BX90g8"}
2020-08-08 16:33:23.446510, Thread-3 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 4}], "csrf_token": "IjkxZGNlN2YyMmUxMjQ0M2Y0MTdjNDQ4ZmQ1MDMxZjkwNjBhNzAzZjMi.Xy63sw.-6buL11No3UJBlElpW4tX4B-lp0"}
2020-08-08 16:33:23.453477, Thread-1 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}], "csrf_token": "IjIxNmU4MDQyZDFmZmIyZDlmZjE4MzNlNDUzYzFjMGYxMWYxYzEwNGYi.Xy63sw.fgs6Cm2owsJf4NjTm7gKrVESabI"}
2020-08-08 16:33:23.457912, Thread-4 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée", "csrf_token": "IjQ0ZDQxODgzN2M5NjRiYWI0NjA2MTk5YWFkNGFhMzY1M2IxNWMyNDIi.Xy63sw.mOa5MKXvJ-EXf_qEok-OqC5j_mg"}
2020-08-08 16:33:23.458442, Thread-4 : fin du calcul de l'impôt des 1 contribuables
2020-08-08 16:33:23.459045, Thread-3 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée", "csrf_token": "ImQ0NDZlYmViYjY1ZDUxYzJhMTNmM2JiZTRkMjBjZGJkYzE0OGVkYzMi.Xy63sw.fviTJz4zFDqVLlVlkrosT_JRPww"}
2020-08-08 16:33:23.459700, Thread-3 : fin du calcul de l'impôt des 4 contribuables
2020-08-08 16:33:23.460492, Thread-1 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée", "csrf_token": "Ijg3MjQ1NGUyYTUyOGEyNTdmZmNmYWZkMmU2OTgyMzUwNjI1YTlhZjIi.Xy63sw.I0xBl9Q8DzsuXPSgOdeARc_VKBA"}
2020-08-08 16:33:23.460492, Thread-1 : fin du calcul de l'impôt des 4 contribuables
2020-08-08 16:33:23.460492, MainThread : fin du calcul de l'impôt des contribuables
If we look at the CSRF tokens received in succession, we see that they are all different.