Skip to content

34. Esercizio pratico: versione 14

La cartella [http-servers/09] per la versione 14 si ottiene copiando la cartella [http-servers/08] dalla versione 13.

34.1. Introduzione

Il CSRF (Cross-Site Request Forgery) è una tecnica di dirottamento della sessione. Su Wikipedia (https://fr.wikipedia.org/wiki/Cross-site_request_forgery) viene spiegato come segue:

Supponiamo che Alice sia l'amministratrice di un forum e che abbia effettuato l'accesso tramite un sistema di sessioni. Malorie è un membro dello stesso forum e desidera eliminare uno dei post del forum. Poiché non dispone delle autorizzazioni necessarie con il proprio account, utilizza l'account di Alice tramite un attacco CSRF.
  1. Malorie riesce a trovare il link che le permette di cancellare il messaggio in questione.
  2. Malorie invia un messaggio ad Alice contenente una pseudo-immagine da visualizzare (che in realtà è uno script). L'URL dell'immagine è il link allo script che elimina il messaggio desiderato.
  3. Alice deve avere una sessione aperta nel suo browser per il sito che Malorie sta prendendo di mira. Questo è un prerequisito affinché l'attacco abbia successo in modo silenzioso senza innescare una richiesta di autenticazione che allertarebbe Alice. Questa sessione deve disporre delle autorizzazioni necessarie per eseguire la richiesta distruttiva di Malorie. Non è necessario che una scheda del browser sia aperta sul sito di destinazione, né che il browser sia in esecuzione. È sufficiente che la sessione sia attiva.
  4. Alice legge il messaggio di Malorie; il suo browser utilizza la sessione aperta di Alice e non richiede un'autenticazione interattiva. Tenta di recuperare il contenuto dell'immagine. Nel farlo, il browser attiva il link ed elimina il messaggio, recuperando una pagina web testuale come contenuto dell'immagine. Poiché non riconosce il tipo di immagine associato, non visualizza alcuna immagine e Alice non si rende conto che Malorie le ha appena fatto cancellare un messaggio contro la sua volontà.

Anche spiegata in questo modo, la tecnica CSRF è difficile da comprendere. Disegniamo un diagramma:

Image

  • In [1-2], Alice comunica con il forum (Sito A). Questo forum mantiene una sessione per ogni utente. Il browser di Alice memorizza localmente questo cookie di sessione e lo invia ogni volta che effettua una nuova richiesta al Sito A;
  • In [3], Malorie invia un messaggio ad Alice. Alice lo legge nel suo browser. Il messaggio è in formato HTML e contiene un link a un'immagine sul Sito B. In realtà, questo link rimanda a uno script JavaScript che viene eseguito non appena raggiunge il browser di Alice;
  • Questo script JavaScript invia quindi una richiesta al Sito A. Il browser di Alice invia automaticamente la richiesta insieme al cookie di sessione memorizzato localmente. È qui che avviene l'attacco: Malorie è riuscita ad accedere al Sito A utilizzando le credenziali di sessione di Alice. Da questo momento in poi, indipendentemente da ciò che accadrà, l'attacco ha avuto luogo;

Per contrastare questo tipo di attacco, il Sito A può procedere come segue:

  • Ad ogni scambio [1-2] con Alice, il Sito A invia una chiave, di seguito denominata token CSRF, che Alice deve restituire nella sua richiesta successiva. Pertanto, ad ogni richiesta, Alice deve inviare due informazioni:
    • il cookie di sessione;
    • il token CSRF ricevuto nella risposta alla sua ultima richiesta al Sito A;

È qui che risiede la protezione: mentre il browser invia automaticamente il cookie di sessione al Sito A, non lo fa per il token CSRF. Per questo motivo, lo scambio 6-7 eseguito dallo script di attacco verrà rifiutato perché la richiesta 6 non avrà inviato il token CSRF;

Il sito A può inviare ad Alice il token CSRF in vari modi per un'applicazione HTML:

  • Può inviare una pagina HTML con ogni richiesta in cui tutti i link contengono il token CSRF, ad esempio [http://siteA/chemin/csrf_token]. Quando Alice clicca su uno di questi link durante la richiesta successiva, il Sito A recupererà semplicemente il token CSRF dall'URL della richiesta e verificherà che sia valido. Questo è ciò che verrà fatto in questo caso;
  • per le pagine HTML contenenti un modulo, può inviare il modulo con un campo nascosto [input type='hidden'] contenente il token CSRF. Questo verrà quindi inviato automaticamente con il modulo quando Alice invia la pagina. Il sito A recupererà il token CSRF dal corpo della richiesta;
  • sono possibili altre tecniche;

34.2. Configurazione

Image

Aggiungiamo due valori booleani alla configurazione [parameters] dell'applicazione:

  • [with_redissession]: se impostato su True, l'applicazione utilizza una sessione Redis. Se impostato su False, l'applicazione utilizza una sessione Flask standard;
  • [with_csrftoken]: se impostato su True, gli URL dell'applicazione contengono un token CSRF;

        # 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. Implementazione CSRF

Ci assicureremo che quando:


config['parameters']['with_csrftoken']

è impostato su [True], l'applicazione invii al browser del client pagine web i cui link contengano un token CSRF.

34.3.1. Il modulo [flask_wtf]

Il token CSRF verrà implementato utilizzando il modulo [flask_wtf], che installiamo in un terminale PyCharm:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install flask_wtf
Collecting flask_wtf

34.3.2. Visualizza modelli

Stiamo introducendo una nuova classe nei modelli:

Image

La classe [AbstractBaseModelForView] è la seguente:

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 ""
  • riga 9: la classe [AbstractBaseModelForView] implementa l'interfaccia [InterfaceModelForView] implementata dalle classi modello;
  • righe 11–13: il metodo [get_model_for_view] non è implementato;
  • Righe 15–20: il metodo [get_csrftoken] genera il token CSRF se l'applicazione è stata configurata per utilizzarli. A seconda della situazione, la funzione restituisce un token preceduto da una barra (/) o una stringa vuota. La funzione [generate_csrf] genera sempre lo stesso valore per una data richiesta del client. L'elaborazione di una richiesta comporta l'esecuzione di varie funzioni. L'uso di [generate_csrf] in queste funzioni genera sempre lo stesso valore. Alla richiesta successiva, tuttavia, viene generato un nuovo token CSRF;

Tutti i modelli M per la vista V includeranno il token CSRF come segue:

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
  • Ogni classe di modello estende la classe base [AbstractBaseModelForView];
  • Riga 8: Il token CSRF viene richiesto alla classe padre. Otteniamo una stringa vuota o una stringa simile a [/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c];

34.3.3. Le viste

Da quanto abbiamo appena visto, tutte le viste V avranno il token CSRF nel loro modello M. Possono quindi utilizzarlo nei link che contengono. Vediamo alcuni esempi:

Il frammento di autenticazione [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>
  • riga 2: in base a quanto appena visto, l'URL per l'attributo [action] sarà:

[/authentifier-utilisateur/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c]

oppure

[/authentifier-utilisateur]

a seconda che l'applicazione sia stata configurata per utilizzare i token CSRF;

Il frammento di calcolo delle imposte [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>

La sezione delle simulazioni [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 %}

Il frammento di codice del menu [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. Percorsi

Ora ci sono due tipi di percorsi, a seconda che utilizzino o meno un token CSRF:

Image

  • [routes_without_csrftoken] sono percorsi senza token CSRF. Si tratta dei percorsi della versione precedente;
  • [routes_with_csrftoken] sono percorsi con un token CSRF.

In [routes_with_csrftoken], i percorsi ora hanno un parametro aggiuntivo, il token CSRF:

#  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()

Tutte le rotte ora hanno il token CSRF nei loro parametri, inclusa la rota [/init-session]. Ciò significa che il client non può avviare l'applicazione digitando direttamente l'URL [/init-session/html] perché mancherà il token CSRF. Ora deve passare attraverso l'URL [/] nelle righe 7–10.

I percorsi sono selezionati nello script principale [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__)
  • righe 9–13: selezione dei percorsi a seconda che l'applicazione utilizzi token CSRF;

34.3.5. Il [MainController]

Per ogni richiesta, il server deve verificare la presenza del token CSRF. Lo faremo nel controller principale [MainController], che gestisce tutte le richieste:

from flask_wtf.csrf import generate_csrf, validate_csrf

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

            

            #  path elements are retrieved
            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)
  • Riga 20: Recuperiamo il token CSRF dall'URL della richiesta del modulo [http://machine:port/path/action/param1/param2/…/csrf_token]. Il token di sessione è sempre l'ultimo elemento dell'URL;
  • riga 23: la validità del token CSRF recuperato dall'URL viene verificata rispetto al token CSRF della sessione. Se non è valido, la funzione [validate_csrf] genera un'eccezione [ValidationError] (riga 27);
  • riga 41: il token CSRF viene incluso nella risposta inviata al client. I client JSON e XML ne avranno bisogno. Questo perché tali client non ricevono pagine HTML con il token CSRF nei link contenuti nelle pagine. Lo riceveranno quindi nella risposta JSON o XML inviata dal server;

Nota: la funzione [validate_csrf] alla riga 23 non verifica la corrispondenza esatta. Il token CSRF è memorizzato nella sessione sotto la chiave [csrf_token]. I test sembrano indicare che un token CSRF è valido se è stato generato durante la sessione. Pertanto, se si sostituisce manualmente il token CSRF [xyz] nell'URL visualizzato nel browser — ad esempio, (/lister-simulations/xyz) — con un altro token [abc] ricevuto in precedenza durante un'azione precedente, l'azione [/lister-simulations] avrà esito positivo;

34.4. Test con un browser

Primo:

  • avviare il server con il parametro [with_csrftoken] impostato su [True];
  • richiedere l'URL [http://localhost:5000] utilizzando un browser;

Image

  • in [1], il token CSRF;

Eseguiamo alcune operazioni fino a ottenere un elenco di simulazioni:

Image

Ora, inseriamo manualmente l'URL [http://localhost:5000/supprimer-simulation/1/x] per cancellare la simulazione con id=1. Inseriamo intenzionalmente un token CSRF errato per vedere cosa succede. La risposta del server è la seguente:

Image

Nota 1: non è certo che il metodo qui utilizzato sia sempre sufficiente per contrastare gli attacchi CSRF. Torniamo al diagramma dell'attacco:

Image

Se lo script JavaScript scaricato in [5] è in grado di leggere la cronologia del browser utilizzata da Alice, potrà recuperare gli URL eseguiti dal browser, come [/target/csrf_token]. Potrà quindi recuperare il token di sessione [csrf_token] e sferrare il suo attacco in [6-7]. Tuttavia, il browser consente l'accesso solo alla cronologia della finestra del browser in cui lo script è in esecuzione. Pertanto, se Alice non utilizza la stessa finestra per interagire con il Sito A [1-2] e leggere il messaggio di Malorie [3], l'attacco CSRF non sarà possibile.

34.5. Client console

Un altro modo per testare la versione 14 dell'applicazione è riutilizzare i test della versione 12 e adattarli al nuovo server.

Image

La cartella [impots/http-clients/09] viene inizialmente creata copiando la cartella [impots/http-clients/07]. Viene poi modificata.

Torniamo alle routine che inizializzano una sessione:

#  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()

Nessuno di questi percorsi è adatto per l'inizializzazione di una sessione JSON o XML:

  • righe 2–5: il percorso [/] inizializza una sessione HTML;
  • righe 8–11: il percorso [/init-session] richiede un token CSRF che non conosciamo;

Decidiamo di aggiungere un nuovo percorso al server:

1
2
3
4
5
#  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)
  • riga 2: il nuovo percorso. Non richiede un token CSRF. Siamo quindi tornati al percorso [/init-session] della versione precedente;
  • righe 4-5: reindirizziamo il client (JSON, XML, HTML) al percorso [/init-session], che include il token CSRF nei suoi parametri;

È possibile testare questo nuovo percorso in un browser:

Image

La risposta del server (configurata con [with_csrftoken=True]) è la seguente:

Image

  • in [1], il server è stato reindirizzato alla route [/init-session] con il token CSRF nell'URL;
  • in [2], il token CSRF si trova nel dizionario JSON inviato dal server, associato alla chiave [csrf_token];

Torniamo al codice client:

Image

Modifichiamo la configurazione [config] come segue:


   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'
  • riga 31: un valore booleano indicherà al client se il server a cui si sta rivolgendo utilizza token CSRF o meno;
  • righe 37–40: viene impostato l'URL del servizio per l'azione [init-session]:
    • se il server utilizza token CSRF, l'URL del servizio è [/init-session-without-csrftoken];
    • altrimenti, l'URL del servizio è [/init-session];

È stato introdotto il percorso [/init-session-without-csrftoken]. Consente a un client JSON/XML di avviare una sessione con il server senza disporre di un token CSRF. Il client troverà questo token nella risposta del server.

Modifichiamo quindi la classe [ImpôtsDaoWithHttpSession] che implementa il livello [dao] del cliente:

Image

#  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
        #  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
        #  token CSRF
        self.__csrf_token = None

    # étape request / response
    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]: POST parameters in json
        #  [cookies]: cookies to include 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)

        #  debug mode?
        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)

  • righe 38–92: la gestione del token CSRF avviene principalmente all'interno del metodo [get_response];
  • riga 60: il punto chiave è il parametro [allow_redirects=True]. Questo è il suo valore predefinito, ma volevamo sottolinearlo;

Quando si è in modalità [with_csrftoken=True]:

  • i client iniziano la loro interazione con il server chiamando la route [/init-session_without_csftoken/type_response];
  • il server risponde a questa richiesta con un reindirizzamento alla route [/init-session/type_response/csrf_token];
  • Grazie al parametro [allow_redirects=True], questo reindirizzamento sarà seguito dalle [richieste] del client;
  • il token CSRF si troverà nel risultato recuperato alle righe 72 e 74 associato alla chiave [csrf_token];

Quando si è in modalità [with_csrftoken=False]:

  • (continua)
    • i client iniziano la loro interazione con il server chiamando la route [/init-session/type_response];
    • il server risponde a questa richiesta con un reindirizzamento alla route [/init-session/type_response];
    • grazie al parametro [allow_redirects=True], questo reindirizzamento verrà seguito dal client [requests];
    • non c'è alcun token CSRF da recuperare alle righe 81–82. La proprietà [self.__csrf_token] rimane quindi None (riga 36);
  • righe 51–52: per tutte le richieste successive, il token CSRF, se esiste, viene aggiunto al percorso iniziale;
  • righe 81–82: il nuovo token generato dal server per ogni nuova richiesta del client viene memorizzato localmente per essere restituito alla riga 52 con la richiesta successiva;

Inoltre, il metodo [init_session] cambia leggermente:

    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)

È importante ricordare che abbiamo creato un percorso [/init-session-without-csrftoken/<response-type>] per inizializzare il dialogo client/server senza un token CSRF. Tuttavia, abbiamo visto che il metodo [get_response] chiamato alla riga 12 del codice aggiunge sistematicamente il token CSRF memorizzato in [self.__csrf_token] alla fine dell'URL del servizio. Ecco perché, alla riga 6 del codice, rimuoviamo questo token CSRF se esiste.

Questo è tutto. Per il test, eseguiremo:

  • the console clients [main, main2, main3];
  • le classi di test [Test1HttpClientDaoWithSession] e [Test2HttpClientDaoWithSession];

impostando successivamente il parametro di configurazione [with_csrftoken] su True e poi su False.

Image

Ecco un esempio dei log ottenuti eseguendo il client [main json] con [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

Se osserviamo i token CSRF ricevuti in successione, vediamo che sono tutti diversi.