Skip to content

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):

Suppose Alice is the administrator of a forum and is logged in to it via a session system. Malorie is a member of the same forum and wants to delete one of the forum posts. Since she does not have the necessary permissions with her account, she uses Alice’s account via a CSRF attack.
  1. Malorie manages to find out the link that allows her to delete the message in question.
  2. 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.
  3. 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.
  4. 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:

Image

  • 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

Image

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;

        # thread pause duration in seconds
        "sleep_time": 0,
        # Redis server
        "with_redissession": True,
        "redis": {
            "host": "127.0.0.1",
            "port": 6379
        },
        # CSRF token
        "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:

Image

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, result: 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 [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:


class ModelForAuthenticationView(AbstractBaseModelForView):

    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, result: dict) -> dict:
        # encapsulate the page data in a model
        model = {}
        

        # CSRF token
        model['csrf_token'] = super().get_csrftoken(config)

        # Return the model
        return model
  • 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]


<!-- HTML form - values are submitted using the [authenticate-user] action -->
<form method="post" action="/authenticate-user{{model.csrf_token}}">

    <!-- title -->
    <div class="alert alert-primary" role="alert">
        <h4>Please log in</h4>
    </div>


</form>
  • line 2: based on what we just saw, the URL for the [action] attribute will be:
[/authenticate-user/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c]

or

[/authenticate-user]

depending on whether the application has been configured to use CSRF tokens;

The tax calculation fragment [v-calcul-impot.html]


<!-- HTML form submitted via POST -->
<form method="post" action="/calculate-tax{{model.csrf_token}}">
    <!-- 12-column message on a blue background -->
    <div class="col-md-12">
        <div class="alert alert-primary" role="alert">
            <h4>Fill out the form below and submit it</h4>
        </div>
    </div>
    
</form>

The simulations section [v-liste-simulations.html]


{% if model.simulations is undefined or model.simulations|length==0 %}
<!-- message on blue background -->
<div class="alert alert-primary" role="alert">
    <h4>Your simulation list is empty</h4>
</div>
{% endif %}

{% if model.simulations is defined and model.simulations.length!=0 %}
<!-- message on blue background -->
<div class="alert alert-primary" role="alert">
    <h4>List of your simulations</h4>
</div>

<!-- table of simulations -->
<table class="table table-sm table-hover table-striped">
    
    <!-- table body (displayed data) -->
    <tbody>
    <!-- display each simulation by iterating through the simulation table -->
    {% for simulation in model.simulations %}

    <!-- display a row of the table with 6 columns - <tr> tag -->
    <!-- column 1: row header (simulation number) - <th scope='row'> tag -->
    <!-- column 2: parameter value [married] - <td> tag -->
    <!-- Column 3: Parameter value [children] - <td> tag -->
    <!-- column 4: parameter value [salary] - tag <td> -->
    <!-- column 5: parameter value [tax] (of the tax) - tag <td> -->
    <!-- column 6: parameter value [surcharge] - tag <td> -->
    <!-- column 7: parameter value [discount] - tag <td> -->
    <!-- column 8: parameter value [reduction] - tag <td> -->
    <!-- column 9: parameter value [rate] (of tax) - tag <td> -->
    <!-- column 10: link to delete the simulation - tag <td> -->
    <tr>
        <th scope="row">{{simulation.id}}</th>
        <td>{{simulation.married}}</td>
        <td>{{simulation.children}}</td>
        <td>{{simulation.salary}}</td>
        <td>{{simulation.tax}}</td>
        <td>{{simulation.surcharge}}</td>
        <td>{{simulation.discount}}</td>
        <td>{{simulation.reduction}}</td>
        <td>{{simulation.rate}}</td>
        <td><a href="/supprimer-simulation/{{simulation.id}}{{modèle.csrf_token}}">Delete</a></td>
    </tr>
    {% endfor %}
    </tr>
    </tbody>
</table>
{% endif %}

The menu snippet [v-menu.html]


<!-- Bootstrap menu -->
<nav class="nav flex-column">
    <!-- Displaying a list of HTML links -->
    {% for optionMenu in model.optionsMenu %}
    
    {% endfor %}
</nav>

34.3.4. Routes

There are now two types of routes, depending on whether or not they use a CSRF token:

Image

  • [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:


# 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('/authenticate-user/<string:csrf_token>', methods=['POST'])
def authenticate_user(csrf_token: str) -> tuple:
    # execute the controller associated with the action
    return front_controller()

# calculate-tax
@app.route('/calculate-tax/<string:csrf_token>', methods=['POST'])
def calculate_tax(csrf_token: str) -> tuple:
    # Execute the controller associated with the action
    return front_controller()

# Calculate taxes in batches
@app.route('/calculate-taxes/<string:csrf_token>', methods=['POST'])
def calculate_taxes(csrf_token: str):
    # Execute the controller associated with the action
    return front_controller()

# list-simulations
@app.route('/list-simulations/<string:csrf_token>', methods=['GET'])
def list_simulations(csrf_token: str) -> tuple:
    # execute the controller associated with the action
    return front_controller()

# delete-simulation
@app.route('/delete-simulation/<int:number>/<string:csrf_token>', methods=['GET'])
def delete_simulation(number: int, csrf_token: str) -> tuple:
    # execute the controller associated with the action
    return front_controller()

# end-session
@app.route('/logout/<string:csrf_token>', methods=['GET'])
def end_session(csrf_token: str) -> tuple:
    # execute the controller associated with the action
    return front_controller()

# display-tax-calculation
@app.route('/display-tax-calculation/<string:csrf_token>', methods=['GET'])
def display_tax_calculation(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 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]:



# the main thread no longer needs the logger
logger.close()

# if there was an error, stop
if error:
    sys.exit(2)

# Import the web application routes
if config['parameters']['with_csrftoken']:
    import routes_with_csrftoken as routes
else:
    import routes_without_csrftoken as routes

# Configure routes
routes.config = config

# Start the Flask application
routes.execute(__name__)
  • 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:


from flask_wtf.csrf import generate_csrf, validate_csrf

       # process the request
        try:
            # logger
            logger = Logger(config['parameters']['logsFilename'])

            …

            # retrieve the path elements
            params = request.path.split('/')

            # the action is the first element
            action = params[1]

            …

            if config['parameters']['with_csrftoken']:
                # the csrf_token is the last element of the path
                csrf_token = params.pop()
                # we check the token's validity
                # an exception will be raised if the csrf_token is not the expected one
                validate_csrf(csrf_token)

            …

        except ValidationError as exception:
            # invalid CSRF token
            result = {"action": action, "status": 121, "response": [f"{exception}"]}
            status_code = status.HTTP_400_BAD_REQUEST

        except BaseException as exception:
            # other (unexpected) exceptions
            result = {"action": action, "status": 131, "response": [f"{exception}"]}
            status_code = status.HTTP_400_BAD_REQUEST

        finally:
            pass

        # Add the csrf_token to the result
        result['csrf_token'] = generate_csrf()

        # log the result sent to the client
        log = f"[MainController] {result}\n"
        logger.write(log)
  • 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;

Image

  • in [1], the CSRF token;

Let’s perform some operations until we have a list of simulations:

Image

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:

Image

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:

Image

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.

Image

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 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:


# 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: 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:

Image

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

Image

  • 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:

Image

We modify the [config] configuration as follows:


   config.update({
        # taxpayers file
        "taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
        # results file
        "resultsFilename": f"{script_dir}/../data/output/results.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": "/calculate-tax",
                "get-admindata": "/get-admindata",
                "calculate-tax-in-bulk-mode": "/calculate-taxes",
                "init-session": "/init-session-without-csrftoken",
                "end-session": "/end-session",
                "authenticate-user": "/authenticate-user",
                "get-simulations": "/list-simulations",
                "delete-simulation": "/delete-simulation",
            }
        },
        # debug mode
        "debug": True,
        # csrf_token
        "with_csrftoken": True,
    }
    )

    # init-session route
    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:

Image


# imports
import json

import requests
import xmltodict
from flask_api import status

from AbstractTaxesDao import AbstractTaxesDao
from AdminData import AdminData
from TaxError import TaxError
from TaxDaoInterfaceWithHttpSession import TaxDaoInterfaceWithHttpSession
from TaxPayer import TaxPayer

class TaxDaoWithHttpSession(TaxDaoWithHttpSessionInterface):

    # constructor
    def __init__(self, config: dict):
        # Initialize parent
        AbstractTaxDao.__init__(self, config)
        # storing configuration elements
        # General configuration
        self.__config = config
        # server
        self.__config_server = config["server"]
        # services
        self.__config_services = config["server"]['url_services']
        # debug mode
        self.__debug = config["debug"]
        # logger
        self.__logger = None
        # cookies
        self.__cookies = None
        # session type (json, xml)
        self.__session_type = None
        # CSRF token
        self.__csrf_token = None

    # request/response step
    def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
        # [method]: HTTP GET or POST method
        # [url_service]: service URL
        # [data]: POST parameters in x-www-form-urlencoded format
        # [json]: POST parameters in JSON
        # [cookies]: cookies to include in the request

        # We must have an XML or JSON session; otherwise, we won't be able to handle the response
        if self.__session_type not in ['json', 'xml']:
            raise ImpôtsError(73, "There is no valid session currently active")

        # Add the CSRF token to the service URL
        if self.__csrf_token:
            url_service = f"{url_service}/{self.__csrf_token}"

        # Execute the request
        response = requests.request(method,
                                    url_service,
                                    data=data_value,
                                    json=json_value,
                                    cookies=self.__cookies,
                                    allow_redirects=True)

        # debug mode?
        if self.__debug:
            # logger
            if not self.__logger:
                self.__logger = self.__config['logger']
            # logging
            self.__logger.write(f"{response.text}\n")

        # result
        if self.__session_type == "json":
            result = json.loads(response.text)
        else:  # xml
            result = xmltodict.parse(response.text[39:])['root']

        # retrieve cookies from the response if any are present
        if response.cookies:
            self.__cookies = response.cookies

        # retrieve the CSRF token
        if self.__config['with_csrftoken']:
            self.__csrf_token = result.get('csrf_token', None)

        # status code
        status_code = response.status_code

        # if status code is not 200 OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(35, result['response'])

        # return the result
        return result['response']

    
    def init_session(self, session_type: str):
        # Note the session type
        self.__session_type = session_type

        # Remove the CSRF token from previous requests
        self.__csrf_token = None

        # Request the URL for the init-session action
        url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        # execute request
        self.get_response("GET", url_service)

  • 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:


    def init_session(self, session_type: str):
        # note the session type
        self.__session_type = session_type

        # remove the CSRF token from previous calls
        self.__csrf_token = None

        # request the URL for the init-session action
        url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        # Execute request
        self.get_response("GET", url_service)

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.

Image

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: Start of tax calculation for taxpayers
2020-08-08 16:33:23.317903, Thread-1: Start of tax calculation for the 4 taxpayers
2020-08-08 16:33:23.317903, Thread-2: Start of tax calculation for 2 taxpayers
2020-08-08 16:33:23.317903, Thread-3: Start of tax calculation for 4 taxpayers
2020-08-08 16:33:23.317903, Thread-4: Start of tax calculation for 1 taxpayer
2020-08-08 16:33:23.379221, Thread-2: {"action": "init-session", "status": 700, "response": ["session started with json response type"], "csrf_token": "ImFiZmZkYjZmMzFkZDc2YWRjNWYwOGM0NTBmMGM4ODJjYzViOWI4NGEi.Xy63sw.H5L0--yWsvfaWvggrGw78z5VnN0"}
2020-08-08 16:33:23.381073, Thread-4: {"action": "init-session", "status": 700, "response": ["session started with json response type"], "csrf_token": "ImY5YzQyMjlkYzcyYmM4YmZiMGI0NWY5MjE4MzIzNDExZjc0MGQ3MWQi.Xy63sw.q6olg7IP_g2ro_RBFRCX1BX90g8"}
2020-08-08 16:33:23.386982, Thread-3: {"action": "init-session", "status": 700, "response": ["session started with json response type"], "csrf_token": "IjkxZGNlN2YyMmUxMjQ0M2Y0MTdjNDQ4ZmQ1MDMxZjkwNjBhNzAzZjMi.Xy63sw.-6buL11No3UJBlElpW4tX4B-lp0"}
2020-08-08 16:33:23.390269, Thread-1 : {"action": "init-session", "status": 700, "response": ["session started with json response type"], "csrf_token": "IjIxNmU4MDQyZDFmZmIyZDlmZjE4MzNlNDUzYzFjMGYxMWYxYzEwNGYi.Xy63sw.fgs6Cm2owsJf4NjTm7gKrVESabI"}
2020-08-08 16:33:23.413206, Thread-2 : {"action": "authenticate-user", "status": 200, "response": "Authentication successful", "csrf_token": "ImFiZmZkYjZmMzFkZDc2YWRjNWYwOGM0NTBmMGM4ODJjYzViOWI4NGEi.Xy63sw.H5L0--yWsvfaWvggrGw78z5VnN0"}
2020-08-08 16:33:23.422877, Thread-2: {"action": "calculate-taxes", "status": 1500, "response": [{"married": "no", "children": 3, "salary": 100000, "tax": 16782, "surcharge": 7176, "rate": 0.41, "discount": 0, "reduction": 0, "id": 1}, {"married": "yes", "children": 3, "salary": 100000, "tax": 9,200, "surcharge": 2,180, "rate": 0.3, "discount": 0, "reduction": 0, "id": 2}], "csrf_token": "ImFiZmZkYjZmMzFkZDc2YWRjNWYwOGM0NTBmMGM4ODJjYzViOWI4NGEi.Xy63sw.H5L0--yWsvfaWvggrGw78z5VnN0"}
2020-08-08 16:33:23.428622, Thread-4 : {"action": "authenticate-user", "status": 200, "response": "Authentication successful", "csrf_token": "ImY5YzQyMjlkYzcyYmM4YmZiMGI0NWY5MjE4MzIzNDExZjc0MGQ3MWQi.Xy63sw.q6olg7IP_g2ro_RBFRCX1BX90g8"}
2020-08-08 16:33:23.429127, Thread-3: {"action": "authenticate-user", "status": 200, "response": "Authentication successful", "csrf_token": "IjkxZGNlN2YyMmUxMjQ0M2Y0MTdjNDQ4ZmQ1MDMxZjkwNjBhNzAzZjMi.Xy63sw.-6buL11No3UJBlElpW4tX4B-lp0"}
2020-08-08 16:33:23.429127, Thread-1 : {"action": "authenticate-user", "status": 200, "response": "Authentication successful", "csrf_token": "IjIxNmU4MDQyZDFmZmIyZDlmZjE4MzNlNDUzYzFjMGYxMWYxYzEwNGYi.Xy63sw.fgs6Cm2owsJf4NjTm7gKrVESabI"}
2020-08-08 16:33:23.429127, Thread-2: {"action": "end-session", "status": 400, "response": "session reset", "csrf_token": "IjU1YjlmZDA0OWRhNTJlODFmYjgyYjlhM2ExYWNhZmUzNTk2NjA5NGIi.Xy63sw.nyNSvkcG6iG0oIMBjtYPo8ySgdw"}
2020-08-08 16:33:23.438519, Thread-2: Tax calculation for the 2 taxpayers completed
2020-08-08 16:33:23.443033, Thread-4: {"action": "calculate-taxes", "status": 1500, "response": [{"married": "yes", "children": 3, "salary": 200000, "tax": 42842, "surcharge": 17283, "rate": 0.41, "discount": 0, "reduction": 0, "id": 1}], "csrf_token": "ImY5YzQyMjlkYzcyYmM4YmZiMGI0NWY5MjE4MzIzNDExZjc0MGQ3MWQi.Xy63sw.q6olg7IP_g2ro_RBFRCX1BX90g8"}
2020-08-08 16:33:23.446510, Thread-3: {"action": "calculate-taxes", "status": 1500, "response": [{"married": "yes", "children": 5, "salary": 100000, "tax": 4230, "surcharge": 0, "rate": 0.14, "discount": 0, "reduction": 0, "id": 1}, {"married": "no", "children": 0, "salary": 100000, "tax": 22986, "surcharge": 0, "rate": 0.41, "discount": 0, "reduction": 0, "id": 2}, {"married": "yes", "children": 2, "salary": 30000, "tax": 0, "surcharge": 0, "rate": 0.0, "discount": 0, "reduction": 0, "id": 3}, {"married": "no", "children": 0, "salary": 200000, "tax": 64210, "surcharge": 7498, "rate": 0.45, "discount": 0, "reduction": 0, "id": 4}], "csrf_token": "IjkxZGNlN2YyMmUxMjQ0M2Y0MTdjNDQ4ZmQ1MDMxZjkwNjBhNzAzZjMi.Xy63sw.-6buL11No3UJBlElpW4tX4B-lp0"}
2020-08-08 16:33:23.453477, Thread-1: {"action": "calculate-taxes", "status": 1500, "response": [{"married": "yes", "children": 2, "salary": 55555, "tax": 2814, "surcharge": 0, "rate": 0.14, "discount": 0, "reduction": 0, "id": 1}, {"married": "yes", "children": 2, "salary": 50000, "tax": 1384, "surcharge": 0, "rate": 0.14, "discount": 384, "reduction": 347, "id": 2}, {"married": "yes", "children": 3, "salary": 50000, "tax": 0, "surcharge": 0, "rate": 0.14, "discount": 720, "reduction": 0, "id": 3}, {"married": "no", "children": 2, "salary": 100000, "tax": 19884, "surcharge": 4480, "rate": 0.41, "discount": 0, "reduction": 0, "id": 4}], "csrf_token": "IjIxNmU4MDQyZDFmZmIyZDlmZjE4MzNlNDUzYzFjMGYxMWYxYzEwNGYi.Xy63sw.fgs6Cm2owsJf4NjTm7gKrVESabI"}
2020-08-08 16:33:23.457912, Thread-4: {"action": "end-session", "status": 400, "response": "session reset", "csrf_token": "IjQ0ZDQxODgzN2M5NjRiYWI0NjA2MTk5YWFkNGFhMzY1M2IxNWMyNDIi.Xy63sw.mOa5MKXvJ-EXf_qEok-OqC5j_mg"}
2020-08-08 16:33:23.458442, Thread-4: End of tax calculation for 1 taxpayer
2020-08-08 16:33:23.459045, Thread-3: {"action": "end-session", "status": 400, "response": "session reset", "csrf_token": "ImQ0NDZlYmViYjY1ZDUxYzJhMTNmM2JiZTRkMjBjZGJkYzE0OGVkYzMi.Xy63sw.fviTJz4zFDqVLlVlkrosT_JRPww"}
2020-08-08 16:33:23.459700, Thread-3: Tax calculation for the 4 taxpayers completed
2020-08-08 16:33:23.460492, Thread-1: {"action": "end-session", "status": 400, "response": "session reset", "csrf_token": "Ijg3MjQ1NGUyYTUyOGEyNTdmZmNmYWZkMmU2OTgyMzUwNjI1YTlhZjIi.Xy63sw.I0xBl9Q8DzsuXPSgOdeARc_VKBA"}
2020-08-08 16:33:23.460492, Thread-1: Tax calculation for the 4 taxpayers complete
2020-08-08 16:33:23.460492, MainThread: End of tax calculation for taxpayers

If we look at the CSRF tokens received in succession, we see that they are all different.