34. Application Exercise: version 14
The [http-servers/09] file from version 14 is obtained by copying the [http-servers/08] file 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 Alice a message containing a pseudo-image to display (which is actually a script). The URL in the image is the link to the script that deletes the desired message.
- Alice must have an open session in her browser for the site targeted by Malorie. 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 using her browser. The message is in the format HTML and contains a link to an image on Site B. In fact, this link is a link to a script Javascript that executes once it reaches Alice’s browser;
- this Javascript script then sends 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 queried site A using Alice’s session credentials. After that, 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 token CSRF, which Alice must return in her next request. Thus, with each request, Alice must send two pieces of information:
- the session cookie;
- the token CSRF received in the response to her last request to Site A;
The security measure is as follows: while the browser automatically sends the session cookie back to Site A, it does not do so for the token CSRF. For this reason, the exchange between requests 6 and 7 performed by the attack script will be rejected because request 6 will not have sent the token CSRF;
Site A can send Alice the token CSRF in various ways for an application HTML:
- it can send a page HTML with every request, where all links will contain the token CSRF, for example [http://siteA/chemin/csrf_token]. On the next request, when Alice clicks on one of these links, Site A will simply need to retrieve the token CSRF from the URL in the request and verify that it is correct. This is what will be done here;
- for pages containing a form, it can send the form with a hidden field containing the token. This token will then be automatically submitted with the form when Alice submits the page. Site A will retrieve the token CSRF in 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 URL contain a CSRF token;
# thread pause time in seconds
"sleep_time": 0,
# redis server
"with_redissession": True,
"redis": {
"host": "127.0.0.1",
"port": 6379
},
# token csrf
"with_csrftoken": False,
34.3. Implementation CSRF
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 implementation of the CSRF token will be done 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 Models
We are introducing a new class in the models:

The [AbstractBaseModelForView] class is as follows:
from abc import abstractmethod
from flask import Request
from flask_wtf.csrf import generate_csrf
from werkzeug.local import LocalProxy
from InterfaceModelForView import InterfaceModelForView
class AbstractBaseModelForView(InterfaceModelForView):
@abstractmethod
def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
pass
def get_csrftoken(self, config: dict):
# csrf_token
if config['parameters']['with_csrftoken']:
return f"/{generate_csrf()}"
else:
return ""
- line 9: the class [AbstractBaseModelForView] implements the interface [InterfaceModelForView] 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 case, the function returns a token preceded by a /, otherwise an empty string. The [generate_csrf] function has the particularity of always generating 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 token CSRF is generated;
All M models for view V will include the token CSRF as follows:
class ModelForAuthentificationView(AbstractBaseModelForView):
def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
# we encapsulate the paged data in the model
modèle = {}
…
# csrf token
modèle['csrf_token'] = super().get_csrftoken(config)
# we render the model
return modèle
- Each model class extends the base class [AbstractBaseModelForView];
- line 8: the token CSRF 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 V views will have the token CSRF in their M model. 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 values with [authentifier-utilisateur] 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
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 simulation fragment [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 [marié] - <td> tag -->
<!-- column 3: parameter value [enfants] - <td> tag -->
<!-- column 4: parameter value [salaire] - <td> tag -->
<!-- column 5: [impôt] (tax) parameter value - <td> tag -->
<!-- column 6: parameter value [surcôte] - <td> tag -->
<!-- column 7: parameter value [décôte] - <td> tag -->
<!-- column 8: parameter value [réduction] - <td> tag -->
<!-- column 9: [taux] (tax) parameter value - <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 [v-menu.html] menu fragment
<!-- 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 token CSRF:

- [routes_without_csrftoken] are routes without a CSRF token. These are the routes from the previous version;
- [routes_with_csrftoken] are the routes with the CSRF token.
In [routes_with_csrftoken], the routes now have an additional parameter, the CSRF token:
# the front controller
def front_controller() -> tuple:
# forward the request to the main controller
main_controller = config['mvc']['controllers']['main-controller']
return main_controller.execute(request, session, config)
@app.route('/', methods=['GET'])
def index() -> tuple:
# redirect to /init-session/html
return redirect(url_for("init_session", type_response="html", csrf_token=generate_csrf()), status.HTTP_302_FOUND)
# init-session
@app.route('/init-session/<string:type_response>/<string:csrf_token>', methods=['GET'])
def init_session(type_response: str, csrf_token: str) -> tuple:
# execute the controller associated with the action
return front_controller()
# authenticate-user
@app.route('/authentifier-utilisateur/<string:csrf_token>', methods=['POST'])
def authentifier_utilisateur(csrf_token: str) -> tuple:
# execute the controller associated with the action
return front_controller()
# calculate-tax
@app.route('/calculer-impot/<string:csrf_token>', methods=['POST'])
def calculer_impot(csrf_token: str) -> tuple:
# execute the controller associated with the action
return front_controller()
# batch tax calculation
@app.route('/calculer-impots/<string:csrf_token>', methods=['POST'])
def calculer_impots(csrf_token: str):
# execute the controller associated with the action
return front_controller()
# lister-simulations
@app.route('/lister-simulations/<string:csrf_token>', methods=['GET'])
def lister_simulations(csrf_token: str) -> tuple:
# execute the controller associated with the action
return front_controller()
# delete-simulation
@app.route('/supprimer-simulation/<int:numero>/<string:csrf_token>', methods=['GET'])
def supprimer_simulation(numero: int, csrf_token: str) -> tuple:
# execute the controller associated with the action
return front_controller()
# end of session
@app.route('/fin-session/<string:csrf_token>', methods=['GET'])
def fin_session(csrf_token: str) -> tuple:
# execute the controller associated with the action
return front_controller()
# display-calculation-tax
@app.route('/afficher-calcul-impot/<string:csrf_token>', methods=['GET'])
def afficher_calcul_impot(csrf_token: str) -> tuple:
# execute the controller associated with the action
return front_controller()
# get-admindata
@app.route('/get-admindata/<string:csrf_token>', methods=['GET'])
def get_admindata(csrf_token: str) -> tuple:
# execute the controller associated with the action
return front_controller()
All routes now have the token CSRF in their parameters, even the route [/init-session]. This means that the client cannot launch the application by directly typing URL [/init-session/html] because the token CSRF will be missing. They must now go through the URL and [/] in lines 7–10.
The route selection is made in the main script [main]:
…
# the main thread no longer needs the logger
logger.close()
# if there has been an error, we stop
if erreur:
sys.exit(2)
# import routes from web application
if config['parameters']['with_csrftoken']:
import routes_with_csrftoken as routes
else:
import routes_without_csrftoken as routes
# route configuration
routes.config = config
# start Flask application
routes.execute(__name__)
- lines 9–13: route selection depending on whether the application uses CSRF tokens;
34.3.5. The [MainController] controller
For each request, the server must verify the presence of the token CSRF. We will do this in the main controller [MainController], which handles all requests:
from flask_wtf.csrf import generate_csrf, validate_csrf
…
# the request is processed
try:
# logger
logger = Logger(config['parameters']['logsFilename'])
…
# retrieve elements from path
params = request.path.split('/')
# action is the 1st element
action = params[1]
…
if config['parameters']['with_csrftoken']:
# the csrf_token is the last element of the path
csrf_token = params.pop()
# check token validity
# an exception will be thrown if the csrf_token is not the expected one
validate_csrf(csrf_token)
…
except ValidationError as exception:
# csrf token invalid
résultat = {"action": action, "état": 121, "réponse": [f"{exception}"]}
status_code = status.HTTP_400_BAD_REQUEST
except BaseException as exception:
# other (unexpected) exceptions
résultat = {"action": action, "état": 131, "réponse": [f"{exception}"]}
status_code = status.HTTP_400_BAD_REQUEST
finally:
pass
# add the csrf_token to the result
résultat['csrf_token'] = generate_csrf()
# we log the result sent to the customer
log = f"[MainController] {résultat}\n"
logger.write(log)
- Line 20: The token CSRF is retrieved from the URL in the [http://machine :port/chemin/action/param1/param2/…/csrf_token] request. The session token is always the last element of the URL;
- line 23: the validity of the CSRF token retrieved from the URL is verified against the CSRF token from the session. If it is invalid, the [validate_csrf] function throws a [ValidationError] exception (line 27);
- line 41: the token CSRF is placed in the result sent to the client. The clients, jSON, and XML will need it. In fact, 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 result sent by the server;
Note: The [validate_csrf] function on line 23 does not check for an exact match. The token CSRF is stored in the session with the key [csrf_token]. Tests seem to show that a token CSRF is valid if it was generated during the session. Thus, if manually, in the URL displayed in the browser, for example (/lister-simulations/xyz), you replace the token [xyz] CSRF with another token [abc] already received during a previous action, the action [/lister-simulations] will succeed;
34.4. Tests with a browser
On:
- start the server with the parameter [with_csrftoken] set to [True];
- request the URL [http://localhost:5000] using a browser;

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

Now, let’s manually enter URL [http://localhost:5000/supprimer-simulation/1/x] to delete the simulation of id=1. We intentionally enter an incorrect token, CSRF, 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 as [5] is capable of reading the browser history used by Alice, it will be able to retrieve the URL files executed by the browser, such as URL and [/cible/csrf_token]. It can then retrieve the session token [csrf_token] and carry out its attack in [6-7]. However, the browser only allows the history of the browser window in which the script is running to be exploited. Therefore, if Alice does not use the same window to interact with site A ([1-2]) and read Malorie’s message ([3]), the attack (CSRF) will not be possible.
34.5. Clients console
Another way to test the application’s version 14 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:
# application root
@app.route('/', methods=['GET'])
def index() -> tuple:
# redirect to /init-session/html
return redirect(url_for("init_session", type_response="html", csrf_token=generate_csrf()), status.HTTP_302_FOUND)
# init-session-with-csrf-token
@app.route('/init-session/<string:type_response>/<string:csrf_token>', methods=['GET'])
def init_session(type_response: str, csrf_token: str) -> tuple:
# execute the controller associated with the action
return front_controller()
None of these routes are suitable for initializing a jSON or XML session:
- lines 2–5: The route [/] initializes a session HTML;
- lines 8–11: the route [/init-session] requires a token CSRF that we do not know;
We decide to add a new route to the server:
# init-session-without-csrftoken
@app.route('/init-session-without-csrftoken/<string:type_response>', methods=['GET'])
def init_session_without_csrftoken(type_response: str) -> tuple:
# redirect to /init-session/type_response
return redirect(url_for("init_session", type_response=type_response, csrf_token=generate_csrf()), status.HTTP_302_FOUND)
- 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: the client is redirected (jSON, XML, HTML) to the route [/init-session], which has the token CSRF in its parameters;
You can test this new route using a browser:

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

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

We modify the [config] configuration as follows:
config.update({
# taxpayer file
"taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
# results file
"resultsFilename": f"{script_dir}/../data/output/résultats.json",
# error file
"errorsFilename": f"{script_dir}/../data/output/errors.txt",
# log file
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# tax calculation server
"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 token for the action is set to URL:
- if the server uses CSRF tokens, then the service URL is [/init-session-without-csrftoken];
- otherwise, the URL service is [/init-session];
The route [/init-session-without-csrftoken] has been presented. It allows a client with jSON / XML to start a session with the server without possessing 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:

# imports
import json
import requests
import xmltodict
from flask_api import status
from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsDaoWithHttpSession import InterfaceImpôtsDaoWithHttpSession
from TaxPayer import TaxPayer
class ImpôtsDaoWithHttpSession(InterfaceImpôtsDaoWithHttpSession):
# manufacturer
def __init__(self, config: dict):
# parent initialization
AbstractImpôtsDao.__init__(self, config)
# saving configuration items
# config general
self.__config = config
# server
self.__config_server = config["server"]
# services
self.__config_services = config["server"]['url_services']
# mode debug
self.__debug = config["debug"]
# logger
self.__logger = None
# cookies
self.__cookies = None
# session type (json, xml)
self.__session_type = None
# token CSRF
self.__csrf_token = None
# request / response stage
def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
# [method]: HTTP GET or POST method
# [url_service] : URL of service
# [data]: POST parameters in x-www-form-urlencoded
# [json]: parameters from POST to json
# [cookies]: cookies to be included in the request
# you must have a XML or JSON session, otherwise you won't be able to handle the response
if self.__session_type not in ['json', 'xml']:
raise ImpôtsError(73, "il n'y a pas de session valide en cours")
# we add the CSRF token to the URL service token
if self.__csrf_token:
url_service = f"{url_service}/{self.__csrf_token}"
# query execution
response = requests.request(method,
url_service,
data=data_value,
json=json_value,
cookies=self.__cookies,
allow_redirects=True)
# mode debug ?
if self.__debug:
# logger
if not self.__logger:
self.__logger = self.__config['logger']
# log on
self.__logger.write(f"{response.text}\n")
# result
if self.__session_type == "json":
résultat = json.loads(response.text)
else: # xml
résultat = xmltodict.parse(response.text[39:])['root']
# retrieve response cookies, if any
if response.cookies:
self.__cookies = response.cookies
# we retrieve the CSRF token
if self.__config['with_csrftoken']:
self.__csrf_token = résultat.get('csrf_token', None)
# status code
status_code = response.status_code
# if status code other than 200 OK
if status_code != status.HTTP_200_OK:
raise ImpôtsError(35, résultat['réponse'])
# we return the result
return résultat['réponse']
def init_session(self, session_type: str):
# note the session type
self.__session_type = session_type
# delete the CSRF token from previous calls
self.__csrf_token = None
# the URL of the init-session action is requested
url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
# request execution
self.get_response("GET", url_service)
…
- lines 38–92: management of the CSRF token occurs primarily within the [get_response] method;
- line 60: the important point is the [allow_redirects=True] parameter. This is its default value, but we wanted to highlight it;
When in [with_csrftoken=True] mode:
- the clientss begin their dialogue with the server by calling the route [/init-session_without_csftoken/type_response];
- the server responds to this request with a redirect to the [/init-session/type_response/csrf_token] route;
- due to the [allow_redirects=True] parameter, this redirection will be followed by the [requests] client;
- the token CSRF 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)
- the clients tokens begin their dialogue with the server by calling the route [/init-session /type_response];
- the server responds to this request with a redirect to the [/init-session/type_response] route;
- due to the [allow_redirects=True] parameter, this redirection will be followed by the [requests] client;
- there is no token CSRF to retrieve on lines 81–82. The property [self.__csrf_token] therefore remains at None (line 36);
- lines 51–52: for all subsequent requests, the token CSRF, if it exists, is appended 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 for the next request;
Additionally, the [init_session] method changes slightly:
def init_session(self, session_type: str):
# note the session type
self.__session_type = session_type
# delete the CSRF token from previous calls
self.__csrf_token = None
# the URL of the init-session action is requested
url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
# request execution
self.get_response("GET", url_service)
It is important to note here that we created a route named [/init-session-without-csrftoken/<type-response>] to initialize the client/server dialogue without a token (CSRF). However, we have seen that the [get_response] method called on line 12 of the code systematically adds the token CSRF, stored in [self.__csrf_token], to the end of the URL service token. That is why, on line 6 of the code, we remove this token CSRF if it existed.
That's it. For testing, run:
- clients console [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.