Skip to content

34. Praktische Übung: Version 14

Der Ordner [http-servers/09] für Version 14 wird durch Kopieren des Ordners [http-servers/08] aus Version 13 erstellt.

34.1. Einführung

CSRF (Cross-Site Request Forgery) ist eine Technik zum Session-Hijacking. Auf Wikipedia (https://fr.wikipedia.org/wiki/Cross-site_request_forgery) wird dies wie folgt erklärt:

Angenommen, Alice ist Administratorin eines Forums und über ein Sitzungssystem dort angemeldet. Malorie ist Mitglied desselben Forums und möchte einen der Forenbeiträge löschen. Da sie mit ihrem Konto nicht über die erforderlichen Berechtigungen verfügt, nutzt sie Alices Konto mittels eines CSRF-Angriffs.
  1. Malorie gelingt es, den Link zu finden, über den sie die betreffende Nachricht löschen kann.
  2. Malorie sendet Alice eine Nachricht, die ein anzuzeigendes Pseudo-Bild enthält (das in Wirklichkeit ein Skript ist). Die URL des Bildes ist der Link zu dem Skript, das die gewünschte Nachricht löscht.
  3. Alice muss in ihrem Browser eine offene Sitzung für die Website haben, auf die Malorie abzielt. Dies ist eine Voraussetzung dafür, dass der Angriff unbemerkt gelingt, ohne eine Authentifizierungsanfrage auszulösen, die Alice alarmieren würde. Diese Sitzung muss über die erforderlichen Berechtigungen verfügen, um Malories zerstörerische Anfrage auszuführen. Es ist nicht notwendig, dass ein Browser-Tab auf der Zielseite geöffnet ist, noch muss der Browser überhaupt laufen. Es reicht aus, dass die Sitzung aktiv ist.
  4. Alice liest Malories Nachricht; ihr Browser nutzt Alices offene Sitzung und fordert keine interaktive Authentifizierung an. Er versucht, den Inhalt des Bildes abzurufen. Dabei löst der Browser den Link aus und löscht die Nachricht, wobei er eine textbasierte Webseite als Bildinhalt abruft. Da er den zugehörigen Bildtyp nicht erkennt, zeigt er kein Bild an, und Alice merkt nicht, dass Malorie sie gerade dazu gebracht hat, eine Nachricht gegen ihren Willen zu löschen.

Selbst so erklärt ist die CSRF-Technik schwer zu verstehen. Zeichnen wir ein Diagramm:

Image

  • In [1-2] kommuniziert Alice mit dem Forum (Website A). Dieses Forum unterhält für jeden Benutzer eine Sitzung. Alices Browser speichert dieses Sitzungs-Cookie lokal und sendet es jedes Mal zurück, wenn er eine neue Anfrage an Website A stellt;
  • In [3] sendet Malorie eine Nachricht an Alice. Alice liest sie in ihrem Browser. Die Nachricht ist im HTML-Format und enthält einen Link zu einem Bild auf Website B. Tatsächlich ist dieser Link ein Link zu einem JavaScript-Skript, das ausgeführt wird, sobald es Alices Browser erreicht;
  • Dieses JavaScript-Skript sendet daraufhin eine Anfrage an Website A. Alices Browser sendet die Anfrage automatisch zusammen mit dem lokal gespeicherten Sitzungscookie. Hier findet der Angriff statt: Malorie hat mit Alices Sitzungsdaten erfolgreich auf Website A zugegriffen. Ab diesem Zeitpunkt ist der Angriff, unabhängig davon, was noch geschieht, erfolgreich;

Um dieser Art von Angriff entgegenzuwirken, kann Website A wie folgt vorgehen:

  • Bei jedem Austausch [1-2] mit Alice sendet Website A einen Schlüssel, im Folgenden als CSRF-Token bezeichnet, den Alice in ihrer nächsten Anfrage zurücksenden muss. Somit muss Alice bei jeder Anfrage zwei Informationen senden:
    • das Sitzungs-Cookie;
    • das CSRF-Token, das sie in der Antwort auf ihre letzte Anfrage an Website A erhalten hat;

Hierin liegt der Schutz: Während der Browser das Sitzungscookie automatisch an Website A zurücksendet, tut er dies nicht für das CSRF-Token. Aus diesem Grund wird der vom Angriffsskript durchgeführte Austausch 6-7 abgelehnt, da in Anfrage 6 das CSRF-Token nicht gesendet wurde;

Website A kann Alice das CSRF-Token auf verschiedene Weise für eine HTML-Anwendung senden:

  • Sie kann bei jeder Anfrage eine HTML-Seite senden, auf der alle Links das CSRF-Token enthalten, zum Beispiel [http://siteA/chemin/csrf_token]. Wenn Alice bei der nächsten Anfrage auf einen dieser Links klickt, ruft Website A einfach das CSRF-Token aus der Anfrage-URL ab und überprüft dessen Gültigkeit. Genau das wird hier geschehen;
  • Bei HTML-Seiten, die ein Formular enthalten, kann sie das Formular mit einem versteckten Feld [input type='hidden'] senden, das das CSRF-Token enthält. Dieses wird dann automatisch mit dem Formular übermittelt, wenn Alice die Seite absendet. Website A ruft das CSRF-Token aus dem Hauptteil der Anfrage ab;
  • Es sind auch andere Techniken möglich;

34.2. Konfiguration

Image

Wir fügen der [parameters]-Konfiguration der Anwendung zwei boolesche Werte hinzu:

  • [with_redissession]: Wenn auf „True“ gesetzt, verwendet die Anwendung eine Redis-Sitzung. Wenn auf „False“ gesetzt, verwendet die Anwendung eine Standard-Flask-Sitzung;
  • [with_csrftoken]: Wenn auf „True“ gesetzt, enthalten die URLs der Anwendung ein CSRF-Token;

        # durée pause thread en secondes
        "sleep_time"0,
        # serveur Redis
        "with_redissession"True,
        "redis": {
            "host""127.0.0.1",
            "port"6379
        },
        # token csrf
        "with_csrftoken"False,

34.3. CSRF-Implementierung

Wir stellen sicher, dass, wenn:


config['parameters']['with_csrftoken']

auf [True] gesetzt ist, die Anwendung Webseiten an den Browser des Clients sendet, deren Links ein CSRF-Token enthalten.

34.3.1. Das Modul [flask_wtf]

Das CSRF-Token wird mithilfe des Moduls [flask_wtf] implementiert, das wir in einem PyCharm-Terminal installieren:


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

34.3.2. Ansichtsvorlagen

Wir führen eine neue Klasse in den Modellen ein:

Image

Die Klasse [AbstractBaseModelForView] sieht wie folgt aus:

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 ""
  • Zeile 9: Die Klasse [AbstractBaseModelForView] implementiert die Schnittstelle [InterfaceModelForView], die von den Modellklassen implementiert wird;
  • Zeilen 11–13: Die Methode [get_model_for_view] ist nicht implementiert;
  • Zeilen 15–20: Die Methode [get_csrftoken] generiert das CSRF-Token, sofern die Anwendung für dessen Verwendung konfiguriert wurde. Je nach Situation gibt die Funktion ein Token zurück, dem ein Schrägstrich (/) vorangestellt ist, oder eine leere Zeichenkette. Die Funktion [generate_csrf] generiert für eine bestimmte Client-Anfrage immer denselben Wert. Die Verarbeitung einer Anfrage umfasst die Ausführung verschiedener Funktionen. Die Verwendung von [generate_csrf] in diesen Funktionen erzeugt immer denselben Wert. Bei der nächsten Anfrage wird jedoch ein neues CSRF-Token generiert;

Alle M-Modelle für die Ansicht V enthalten das CSRF-Token wie folgt:

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
  • Jede Modellklasse erweitert die Basisklasse [AbstractBaseModelForView];
  • Zeile 8: Das CSRF-Token wird von der übergeordneten Klasse angefordert. Wir erhalten entweder eine leere Zeichenfolge oder eine Zeichenfolge wie [/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c];

34.3.3. Die Ansichten

Nach dem, was wir gerade gesehen haben, verfügen alle Ansichten V über das CSRF-Token in ihrer Vorlage M. Sie können es daher in den darin enthaltenen Links verwenden. Sehen wir uns einige Beispiele an:

Das Authentifizierungsfragment [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>
  • Zeile 2: Basierend auf dem, was wir gerade gesehen haben, lautet die URL für das [action]-Attribut:

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

oder

[/authentifier-utilisateur]

je nachdem, ob die Anwendung für die Verwendung von CSRF-Tokens konfiguriert wurde;

Das Fragment zur Steuerberechnung [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>

Der Abschnitt „Simulationen“ [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 %}

Das Menü-Snippet [v-menu.html]


<!-- bootstrap menu -->
<nav class="nav flex-column">
    <!-- display a list of links HTML -->
    {% for optionMenu in modèle.optionsMenu %}
    <a class="nav-link" href="{{optionMenu.url}}{{modèle.csrf_token}}">{{optionMenu.text}}</a>
    {% endfor %}
</nav>

34.3.4. Routen

Es gibt nun zwei Arten von Routen, je nachdem, ob sie ein CSRF-Token verwenden oder nicht:

Image

  • [routes_without_csrftoken] sind Routen ohne CSRF-Token. Dies sind die Routen aus der vorherigen Version;
  • [routes_with_csrftoken] sind Routen mit einem CSRF-Token.

In [routes_with_csrftoken] verfügen Routen nun über einen zusätzlichen Parameter, das 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()

Alle Routen enthalten nun das CSRF-Token in ihren Parametern, einschließlich der Route [/init-session]. Das bedeutet, dass der Client die Anwendung nicht durch direkte Eingabe der URL [/init-session/html] starten kann, da das CSRF-Token fehlen würde. Er muss nun die URL [/] in den Zeilen 7–10 durchlaufen.

Die Routen werden im Hauptskript [main] ausgewählt:


#  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__)
  • Zeilen 9–13: Auswahl der Routen je nachdem, ob die Anwendung CSRF-Token verwendet;

34.3.5. Der [MainController]

Bei jeder Anfrage muss der Server das Vorhandensein des CSRF-Tokens überprüfen. Dies erfolgt im Hauptcontroller [MainController], der alle Anfragen verarbeitet:

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)
  • Zeile 20: Abrufen des CSRF-Tokens aus der Anfrage-URL des Formulars [http://machine:port/path/action/param1/param2/…/csrf_token]. Das Session-Token ist immer das letzte Element der URL;
  • Zeile 23: Die Gültigkeit des aus der URL abgerufenen CSRF-Tokens wird mit dem CSRF-Token der Sitzung abgeglichen. Ist es ungültig, löst die Funktion [validate_csrf] eine [ValidationError]-Ausnahme aus (Zeile 27);
  • Zeile 41: Das CSRF-Token wird in die an den Client gesendete Antwort aufgenommen. JSON- und XML-Clients benötigen es. Der Grund dafür ist, dass diese Clients keine HTML-Seiten mit dem CSRF-Token in den auf den Seiten enthaltenen Links erhalten. Sie erhalten es daher in der vom Server gesendeten JSON- oder XML-Antwort;

Hinweis: Die Funktion [validate_csrf] in Zeile 23 prüft nicht auf eine exakte Übereinstimmung. Das CSRF-Token wird in der Sitzung unter dem Schlüssel [csrf_token] gespeichert. Tests deuten darauf hin, dass ein CSRF-Token gültig ist, wenn es während der Sitzung generiert wurde. Wenn Sie also das CSRF-Token [xyz] in der im Browser angezeigten URL – zum Beispiel (/lister-simulations/xyz) – manuell durch ein anderes Token [abc] ersetzen, das Sie zuvor bei einer früheren Aktion erhalten haben, wird die Aktion [/lister-simulations] erfolgreich ausgeführt;

34.4. Tests mit einem Browser

Erstens:

  • Starten Sie den Server mit dem Parameter [with_csrftoken] auf [True] gesetzt;
  • Rufen Sie die URL [http://localhost:5000] über einen Browser auf;

Image

  • in [1] das CSRF-Token;

Führen wir einige Operationen durch, bis wir eine Liste von Simulationen haben:

Image

Geben Sie nun manuell die URL [http://localhost:5000/supprimer-simulation/1/x] ein, um die Simulation mit der ID=1 zu löschen. Wir geben absichtlich ein falsches CSRF-Token ein, um zu sehen, was passiert. Die Antwort des Servers lautet wie folgt:

Image

Anmerkung 1: Es ist nicht sicher, dass die hier verwendete Methode immer ausreicht, um CSRF-Angriffen entgegenzuwirken. Kehren wir zum Angriffsdiagramm zurück:

Image

Wenn das in [5] heruntergeladene JavaScript-Skript in der Lage ist, den von Alice verwendeten Browserverlauf auszulesen, kann es die vom Browser ausgeführten URLs abrufen, wie beispielsweise [/target/csrf_token]. Es kann dann das Sitzungstoken [csrf_token] abrufen und seinen Angriff in [6-7] ausführen. Der Browser erlaubt jedoch nur den Zugriff auf den Verlauf des Browserfensters, in dem das Skript ausgeführt wird. Wenn Alice also nicht dasselbe Fenster verwendet, um mit Website A zu interagieren [1-2] und Malories Nachricht zu lesen [3], ist der CSRF-Angriff nicht möglich.

34.5. Konsolen-Clients

Eine weitere Möglichkeit, Version 14 der Anwendung zu testen, besteht darin, die Tests aus Version 12 wiederzuverwenden und sie an den neuen Server anzupassen.

Image

Der Ordner [impots/http-clients/09] wird zunächst durch Kopieren des Ordners [impots/http-clients/07] erstellt. Anschließend wird er angepasst.

Kommen wir zurück zu den Routen, die eine Sitzung initialisieren:

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

Keine dieser Routen eignet sich für die Initialisierung einer JSON- oder XML-Sitzung:

  • Zeilen 2–5: Die Route [/] initialisiert eine HTML-Sitzung;
  • Zeilen 8–11: Die Route [/init-session] erfordert ein CSRF-Token, das uns nicht bekannt ist;

Wir beschließen, dem Server eine neue Route hinzuzufügen:

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)
  • Zeile 2: die neue Route. Sie erwartet kein CSRF-Token. Wir sind somit zur Route [/init-session] aus der vorherigen Version zurückgekehrt;
  • Zeilen 4–5: Wir leiten den Client (JSON, XML, HTML) zur Route [/init-session] weiter, die das CSRF-Token in ihren Parametern enthält;

Sie können diese neue Route in einem Browser testen:

Image

Die Antwort des Servers (konfiguriert mit [with_csrftoken=True]) lautet wie folgt:

Image

  • In [1] wurde der Server zur Route [/init-session] umgeleitet, wobei das CSRF-Token in der URL enthalten war;
  • in [2] befindet sich das CSRF-Token im vom Server gesendeten JSON-Dictionary, verknüpft mit dem Schlüssel [csrf_token];

Kehren wir zum Client-Code zurück:

Image

Wir ändern die [config]-Konfiguration wie folgt:


   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'
  • Zeile 31: Ein boolescher Wert teilt dem Client mit, ob der Server, an den er sich wendet, mit CSRF-Tokens arbeitet oder nicht;
  • Zeilen 37–40: Die Service-URL für die Aktion [init-session] wird festgelegt:
    • Wenn der Server CSRF-Token verwendet, lautet die Service-URL [/init-session-without-csrftoken];
    • ansonsten lautet die Service-URL [/init-session];

Die Route [/init-session-without-csrftoken] wurde eingeführt. Sie ermöglicht es einem JSON/XML-Client, eine Sitzung mit dem Server zu starten, ohne über ein CSRF-Token zu verfügen. Der Client findet dieses Token in der Antwort des Servers.

Anschließend passen wir die Klasse [ImpôtsDaoWithHttpSession] an, die die [dao]-Schicht des Kunden implementiert:

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)

  • Zeilen 38–92: Die Verarbeitung des CSRF-Tokens erfolgt hauptsächlich innerhalb der Methode [get_response];
  • Zeile 60: Der entscheidende Punkt ist der Parameter [allow_redirects=True]. Dies ist sein Standardwert, aber wir wollten ihn hervorheben;

Im Modus [with_csrftoken=True]:

  • beginnen Clients ihre Interaktion mit dem Server durch Aufruf der Route [/init-session_without_csftoken/type_response];
  • Der Server antwortet auf diese Anfrage mit einer Weiterleitung zur Route [/init-session/type_response/csrf_token];
  • Aufgrund des Parameters [allow_redirects=True] wird diese Weiterleitung vom Client befolgt [requests];
  • Das CSRF-Token befindet sich im abgerufenen Ergebnis in den Zeilen 72 und 74, zugeordnet dem Schlüssel [csrf_token];

Im Modus [with_csrftoken=False]:

  • (Fortsetzung)
    • beginnen die Clients ihre Interaktion mit dem Server durch Aufruf der Route [/init-session/type_response];
    • Der Server antwortet auf diese Anfrage mit einer Weiterleitung zur Route [/init-session/type_response];
    • aufgrund des Parameters [allow_redirects=True] wird diese Weiterleitung vom Client befolgt [requests];
    • in den Zeilen 81–82 gibt es kein CSRF-Token abzurufen. Die Eigenschaft [self.__csrf_token] bleibt daher None (Zeile 36);
  • Zeilen 51–52: Bei allen nachfolgenden Anfragen wird das CSRF-Token, sofern vorhanden, der ursprünglichen Route hinzugefügt;
  • Zeilen 81–82: Das vom Server für jede neue Client-Anfrage generierte neue Token wird lokal gespeichert, um in Zeile 52 mit der nächsten Anfrage zurückgegeben zu werden;

Zudem ändert sich die Methode [init_session] geringfügig:

    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)

Es ist wichtig, sich hier daran zu erinnern, dass wir eine Route [/init-session-without-csrftoken/<response-type>] erstellt haben, um den Client-Server-Dialog ohne CSRF-Token zu initialisieren. Wir haben jedoch gesehen, dass die in Zeile 12 des Codes aufgerufene Methode [get_response] das in [self.__csrf_token] gespeicherte CSRF-Token systematisch an das Ende der Service-URL anhängt. Deshalb entfernen wir in Zeile 6 des Codes dieses CSRF-Token, falls es vorhanden ist.

Das war's. Zum Testen führen wir Folgendes aus:

  • the console clients [main, main2, main3];
  • die Testklassen [Test1HttpClientDaoWithSession] und [Test2HttpClientDaoWithSession];

indem der Konfigurationsparameter [with_csrftoken] nacheinander auf „True“ und dann auf „False“ gesetzt wird.

Image

Hier ist ein Beispiel für die Protokolle, die bei der Ausführung des [main json]-Clients mit [with_csrftoken=True] erhalten werden:


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

Wenn wir uns die nacheinander empfangenen CSRF-Token ansehen, stellen wir fest, dass sie alle unterschiedlich sind.