34. Anwendungsaufgabe: Version 14

Der Ordner [http-servers/09] der Version 14 wird durch Kopieren des Ordners [http-servers/08] der Version 13 erstellt.
34.1. Einleitung
CSRF (Cross-Site-Request-Forgery) ist eine Technik zum Diebstahl von Sitzungen. In Wikipedia (https://fr.wikipedia.org/wiki/Cross-site_request_forgery) wird sie wie folgt erklärt:
- Malorie gelingt es, den Link herauszufinden, über den sich der betreffende Beitrag löschen lässt.
- Malorie sendet Alice eine Nachricht mit einem scheinbar anzuzeigenden Bild (das in Wirklichkeit ein Skript ist). Der URL-Code des Bildes ist der Link zu dem Skript, mit dem sich der gewünschte Beitrag löschen lässt.
- Alice muss in ihrem Browser eine geöffnete Sitzung für die von Malorie ins Visier genommene Website haben. Dies ist eine Voraussetzung dafür, dass der Angriff unbemerkt gelingt, ohne dass eine Authentifizierungsanfrage erforderlich ist, die Alice alarmieren würde. Diese Sitzung muss über die erforderlichen Rechte verfügen, um Malories zerstörerische Anfrage auszuführen. Es ist nicht erforderlich, dass ein Browser-Tab auf der Zielwebsite geöffnet ist oder dass der Browser überhaupt gestartet ist. Es reicht aus, dass die Sitzung aktiv ist.
- Alice liest die Nachricht von Malorie; ihr Browser nutzt die offene Sitzung von Alice 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; er ruft eine Text-Webseite als Inhalt für das Bild ab. Da er den zugehörigen Bildtyp nicht erkennt, zeigt er kein Bild an, und Alice weiß nicht, dass Malorie sie gerade dazu gebracht hat, eine Nachricht gegen ihren Willen zu löschen.
Selbst so erklärt ist die Technik von CSRF schwer zu verstehen. Schauen wir uns ein Schema an:

- In [1-2] kommuniziert Alice mit dem Forum (Website A). Dieses Forum unterhält für jeden Nutzer 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;
- Bei [3] sendet Malorie eine Nachricht an Alice. Diese liest sie mit ihrem Browser. Die gelesene Nachricht hat das Format HTML und enthält einen Link zu einem Bild auf Website B. Tatsächlich handelt es sich bei diesem Link um einen Link zu einem JavaScript-Skript, das ausgeführt wird, sobald es im Browser von Alice ankommt;
- Dieses JavaScript-Skript sendet daraufhin eine Anfrage an die Website A. Alices Browser übermittelt diese Anfrage automatisch zusammen mit dem lokal gespeicherten Sitzungs-Cookie. Genau hier findet der Angriff statt: Malorie ist es gelungen, die Website A mit den Berechtigungen (Sitzung) von Alice abzufragen. Unabhängig davon, was anschließend geschieht, ist der Angriff bereits erfolgt;
Um dieser Art von Angriff entgegenzuwirken, kann die Website A wie folgt vorgehen:
- Bei jedem Datenaustausch [1-2] mit Alice sendet die Website A einen Schlüssel, im Folgenden als Token (Zugangsmarke) CSRF bezeichnet, den Alice bei der nächsten Anfrage zurücksenden muss. Somit muss Alice bei jeder Anfrage zwei Informationen senden:
- das Sitzungs-Cookie;
- das Token CSRF, das sie als Antwort auf ihre letzte Anfrage an die Website A erhalten hat;
Hier liegt der Schutz: Während der Browser das Sitzungs-Cookie automatisch an die Website A zurücksendet, tut er dies nicht für das Token CSRF. Aus diesem Grund wird der vom Angriffsskript durchgeführte Austausch 6-7 abgelehnt, da in Anfrage 6 das Token CSRF nicht gesendet wurde;
Die Website A kann Alice das Token CSRF auf verschiedene Weise für eine Anwendung mit dem Token HTML senden:
- Sie kann bei jeder Anfrage eine Seite mit dem Token HTML senden, auf der alle Links das Token CSRF enthalten, zum Beispiel [http://siteA/chemin/csrf_token]. Bei der nächsten Anfrage, wenn Alice auf einen dieser Links klickt, muss die Website A lediglich das Token CSRF aus dem URL der Anfrage abrufen und überprüfen, ob es korrekt ist. Genau das wird hier getan;
- Bei Seiten mit der ID HTML, die ein Formular enthalten, kann die Website dieses Formular mit einem versteckten Feld [input type=’hidden’] versenden, das das Token CSRF enthält. Dieses wird dann automatisch mit dem Formular übermittelt, sobald Alice die Seite absendet. Die Website A ruft das Token CSRF aus dem Body der Anfrage ab;
- es sind auch andere Techniken denkbar;
34.2. Konfiguration

Wir fügen in die Konfiguration [parameters] der Anwendung zwei boolesche Werte ein:
- [with_redissession]: Bei „True“ verwendet die Anwendung eine Redis-Sitzung. Bei „False“ verwendet die Anwendung eine normale Flask-Sitzung;
- [with_csrftoken]: Bei „True“ enthalten die URL der Anwendung ein CSRF-Token;
# Pausenzeit des Threads in Sekunden
"sleep_time": 0,
# Redis-Server
"with_redissession": True,
"redis": {
"host": "127.0.0.1",
"port": 6379
},
# CSRF-Token
"with_csrftoken": False,
34.3. Implementierung CSRF
Wir werden sicherstellen, dass, wenn:
config['parameters']['with_csrftoken']
den Wert [True] hat, die Anwendung Webseiten an den Client-Browser sendet, deren Links ein Token CSRF enthalten.
34.3.1. Das Modul [flask_wtf]
Die Implementierung des Tokens CSRF erfolgt mit dem Modul [flask_wtf], das wir in einem Terminal PyCharm installieren:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install flask_wtf
Collecting flask_wtf
…
34.3.2. Die Vorlagen für die Ansichten
Wir fügen eine neue Klasse in die Vorlagen ein:

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 Token CSRF, sofern die Anwendung für dessen Verwendung konfiguriert wurde. Je nach Fall gibt die Funktion ein Token zurück, dem ein Schrägstrich (/) vorangestellt ist, andernfalls eine leere Zeichenkette. Die Funktion [generate_csrf] zeichnet sich dadurch aus, dass sie für eine bestimmte Client-Anfrage immer denselben Wert generiert. Die Verarbeitung einer Anfrage umfasst die Ausführung verschiedener Funktionen. Die Verwendung von [generate_csrf] in diesen Funktionen führt immer zum gleichen Wert. Bei der nächsten Anfrage wird hingegen ein neues Token CSRF generiert;
Alle M-Vorlagen der Ansicht V werden das Token CSRF wie folgt einbinden:
class ModelForAuthentificationView(AbstractBaseModelForView):
def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
# Die Daten der Seite werden in eine Vorlage gekapselt
modèle = {}
…
# CSRF-Token
modèle['csrf_token'] = super().get_csrftoken(config)
# Die Vorlage wird zurückgegeben
return modèle
- Jede Vorlagenklasse erweitert die Basisklasse [AbstractBaseModelForView];
- Zeile 8: Das Token CSRF wird von der übergeordneten Klasse angefordert. Man erhält entweder die leere Zeichenkette oder eine Zeichenkette vom Typ [/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c];
34.3.3. Die Ansichten
Wie wir gerade gesehen haben, enthalten alle Ansichten V in ihrem Modell M das Token CSRF. Sie können es daher in den darin enthaltenen Links verwenden. Sehen wir uns einige Beispiele an:
Das Authentifizierungsfragment [v_authentification.html]
<!-- Formular HTML – die Werte werden mit der Aktion [authentifier-utilisateur] übermittelt -->
<form method="post" action="/authentifier-utilisateur{{modèle.csrf_token}}">
<!-- Titel -->
<div class="alert alert-primary" role="alert">
<h4>Veuillez vous authentifier</h4>
</div>
…
</form>
- Zeile 2: Nach dem, was wir gerade gesehen haben, lautet das URL des Attributs [action]:
[/authentifier-utilisateur/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c]
oder
je nachdem, ob die Anwendung für die Verwendung von CSRF-Token konfiguriert wurde oder nicht;
Das Steuerberechnungsfragment [v-calcul-impot.html]
<!-- Formular HTML versendet -->
<form method="post" action="/calculer-impot{{modèle.csrf_token}}">
<!-- Meldung in 12 Spalten auf blauem Hintergrund -->
<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>
Das Simulationsfragment [v-liste-simulations.html]
{% if modèle.simulations is undefined or modèle.simulations|length==0 %}
<!-- Meldung auf blauem Hintergrund -->
<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 %}
<!-- Meldung auf blauem Hintergrund -->
<div class="alert alert-primary" role="alert">
<h4>Liste de vos simulations</h4>
</div>
<!-- Simulationstabelle -->
<table class="table table-sm table-hover table-striped">
…
<!-- Tabelleninhalt (angezeigte Daten) -->
<tbody>
<!-- Jede Simulation wird durch Durchlaufen der Simulationstabelle angezeigt -->
{% for simulation in modèle.simulations %}
<!-- Anzeige einer Zeile der Tabelle mit 6 Spalten – Tag <tr> -->
<!-- Spalte 1: Zeilenüberschrift (Simulationsnummer) – Tag <th scope='row' -->
<!-- Spalte 2: Parameterwert [marié] – Tag <td> -->
<!-- Spalte 3: Parameterwert [enfants] – Tag <td> -->
<!-- Spalte 4: Parameterwert [salaire] – Tag <td> -->
<!-- Spalte 5: Parameterwert [impôt] (Steuer) – Tag <td> -->
<!-- Spalte 6: Parameterwert [surcôte] – Tag <td> -->
<!-- Spalte 7: Parameterwert [décôte] – Tag <td> -->
<!-- Spalte 8: Parameterwert [réduction] – Tag <td> -->
<!-- Spalte 9: Parameterwert [taux] (Steuer) – Tag <td> -->
<!-- Spalte 10: Link zum Löschen der Simulation – Tag <td> -->
<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üfragment [v-menu.html]
<!-- Bootstrap-Menü -->
<nav class="nav flex-column">
<!-- Anzeige einer Liste von 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. Die Routen
Es gibt nun zwei Arten von Routen, je nachdem, ob diese ein Token verwenden oder nicht: CSRF:

- [routes_without_csrftoken] sind die Routen ohne Token CSRF. Dies sind die Routen aus der vorherigen Version;
- [routes_with_csrftoken] sind die Routen mit dem Token CSRF.
In [routes_with_csrftoken] verfügen die Routen nun über einen zusätzlichen Parameter, das Token CSRF:
# der Front-Controller
def front_controller() -> tuple:
# Die Anfrage wird an den Haupt-Controller weitergeleitet
main_controller = config['mvc']['controllers']['main-controller']
return main_controller.execute(request, session, config)
@app.route('/', methods=['GET'])
def index() -> tuple:
# Weiterleitung zu /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:
# Der zur Aktion gehörende Controller wird ausgeführt
return front_controller()
# Benutzer authentifizieren
@app.route('/authentifier-utilisateur/<string:csrf_token>', methods=['POST'])
def authentifier_utilisateur(csrf_token: str) -> tuple:
# Der zur Aktion gehörende Controller wird ausgeführt
return front_controller()
# Steuerberechnung
@app.route('/calculer-impot/<string:csrf_token>', methods=['POST'])
def calculer_impot(csrf_token: str) -> tuple:
# Der zur Aktion gehörende Controller wird ausgeführt
return front_controller()
# Steuerberechnung im Stapelverfahren
@app.route('/calculer-impots/<string:csrf_token>', methods=['POST'])
def calculer_impots(csrf_token: str):
# Der zur Aktion gehörende Controller wird ausgeführt
return front_controller()
# Simulationen auflisten
@app.route('/lister-simulations/<string:csrf_token>', methods=['GET'])
def lister_simulations(csrf_token: str) -> tuple:
# Der mit der Aktion verknüpfte Controller wird ausgeführt
return front_controller()
# Simulation löschen
@app.route('/supprimer-simulation/<int:numero>/<string:csrf_token>', methods=['GET'])
def supprimer_simulation(numero: int, csrf_token: str) -> tuple:
# Der mit der Aktion verknüpfte Controller wird ausgeführt
return front_controller()
# Sitzung beenden
@app.route('/fin-session/<string:csrf_token>', methods=['GET'])
def fin_session(csrf_token: str) -> tuple:
# Der mit der Aktion verknüpfte Controller wird ausgeführt
return front_controller()
# Steuerberechnung anzeigen
@app.route('/afficher-calcul-impot/<string:csrf_token>', methods=['GET'])
def afficher_calcul_impot(csrf_token: str) -> tuple:
# Der zur Aktion gehörende Controller wird ausgeführt
return front_controller()
# Admin-Daten abrufen
@app.route('/get-admindata/<string:csrf_token>', methods=['GET'])
def get_admindata(csrf_token: str) -> tuple:
# Der zur Aktion gehörende Controller wird ausgeführt
return front_controller()
Alle Routen enthalten nun das Token CSRF in ihren Parametern, sogar die Route [/init-session]. Das bedeutet, dass der Kunde die Anwendung nicht starten kann, indem er direkt „URL [/init-session/html]“ eingibt, da dabei das Token „CSRF“ fehlen würde. Er muss nun zwingend über die Routen „URL“ und „[/]“ aus den Zeilen 7–10 gehen.
Die Auswahl der Routen erfolgt im Hauptskript [main]:
…
# Der Hauptthread benötigt den Logger nicht mehr
logger.close()
# Wenn ein Fehler aufgetreten ist, wird der Vorgang beendet
if erreur:
sys.exit(2)
# Import der Routen der Webanwendung
if config['parameters']['with_csrftoken']:
import routes_with_csrftoken as routes
else:
import routes_without_csrftoken as routes
# Konfiguration der Routen
routes.config = config
# Starten der Flask-Anwendung
routes.execute(__name__)
- Zeilen 9–13: Auswahl der Routen je nachdem, ob die Anwendung CSRF-Token verwendet oder nicht;
34.3.5. Der Controller [MainController]
Bei jeder Anfrage muss der Server das Vorhandensein des Tokens CSRF überprüfen. Dies erfolgt im Haupt-Controller [MainController], der alle Anfragen verarbeitet:
from flask_wtf.csrf import generate_csrf, validate_csrf
…
# Die Anfrage wird bearbeitet
try:
# Protokollierung
logger = Logger(config['parameters']['logsFilename'])
…
# Elemente des Pfads abrufen
params = request.path.split('/')
# Die Aktion ist das erste Element
action = params[1]
…
if config['parameters']['with_csrftoken']:
# „csrf_token“ ist das letzte Element des Pfads
csrf_token = params.pop()
# Die Gültigkeit des Tokens wird überprüft
# Es wird eine Ausnahme ausgelöst, wenn das csrf_token nicht dem erwarteten entspricht
validate_csrf(csrf_token)
…
except ValidationError as exception:
# Ungültiges CSRF-Token
résultat = {"action": action, "état": 121, "réponse": [f"{exception}"]}
status_code = status.HTTP_400_BAD_REQUEST
except BaseException as exception:
# Sonstige (unerwartete) Ausnahmen
résultat = {"action": action, "état": 131, "réponse": [f"{exception}"]}
status_code = status.HTTP_400_BAD_REQUEST
finally:
pass
# csrf_token wird dem Ergebnis hinzugefügt
résultat['csrf_token'] = generate_csrf()
# Das an den Client gesendete Ergebnis wird protokolliert
log = f"[MainController] {résultat}\n"
logger.write(log)
- Zeile 20: Wir extrahieren das Token CSRF aus dem URL der Anfrage vom Typ [http://machine :port/chemin/action/param1/param2/…/csrf_token]. Das Sitzungstoken ist immer das letzte Element des URL;
- Zeile 23: Die Gültigkeit des Tokens CSRF, das aus dem URL mit dem Session-Token CSRF der Sitzung abgerufen wurde, wird überprüft. Ist es ungültig, löst die Funktion [validate_csrf] eine Ausnahme vom Typ [ValidationError] aus (Zeile 27);
- Zeile 41: Das Token CSRF wird in das an den Client gesendete Ergebnis aufgenommen. Die Clients jSON und XML benötigen dieses Token. Denn diese Clients erhalten keine HTML-Seiten, in deren Links das Token CSRF enthalten ist. Sie erhalten es daher im vom Server gesendeten Ergebnis jSON oder XML;
Hinweis: Die Funktion [validate_csrf] in Zeile 23 überprüft keine exakte Übereinstimmung. Das Token CSRF wird in der Sitzung mit dem Schlüssel [csrf_token] gespeichert. Tests scheinen zu zeigen, dass ein Token CSRF gültig ist, wenn es im Laufe der Sitzung generiert wurde. Wenn Sie also manuell im im Browser angezeigten URL, zum Beispiel (/lister-simulations/xyz), das Token [xyz] durch ein anderes Token [abc] ersetzen, das Sie bereits bei einer früheren Aktion erhalten haben, wird die Aktion [/lister-simulations] erfolgreich sein;
34.4. Tests mit einem Browser
Man:
- starten Sie den Server mit dem Parameter [with_csrftoken] bis [True];
- ruft die URL URL [http://localhost:5000] mit einem Browser auf;

- in [1] das Token CSRF;
Führen wir einige Bearbeitungen durch, bis wir eine Liste von Simulationen erhalten:

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

Anmerkung 1: Es ist nicht sicher, ob die hier verwendete Methode immer ausreicht, um Angriffe vom Typ CSRF abzuwehren. Kehren wir zum Ablauf des Angriffs zurück:

Wenn das unter [5] heruntergeladene JavaScript-Skript in der Lage ist, den Browserverlauf von Alice auszulesen, kann es die vom Browser ausgeführten URL abrufen, darunter auch URL wie beispielsweise [/cible/csrf_token]. Anschließend kann es das Sitzungstoken [csrf_token] abrufen und seinen Angriff in [6-7] durchführen. Der Browser erlaubt jedoch nur die Auswertung des Verlaufs des Browserfensters, in dem das Skript ausgeführt wird. Wenn Alice also nicht dasselbe Fenster verwendet, um auf der Website A ([1-2]) zu arbeiten und die Nachricht von Malorie ([3]) zu lesen, ist der Angriff (CSRF) nicht möglich.
34.5. Konsolen-Clients
Eine weitere Möglichkeit, Version 14 der Anwendung zu testen, besteht darin, die Tests aus Version 12 zu übernehmen und sie an den neuen Server anzupassen.

Der Ordner [impots/http-clients/09] wird zunächst durch Kopieren des Ordners [impots/http-clients/07] erstellt. Anschließend wird er geändert.
Kommen wir zurück zu den Routen, die eine Sitzung initialisieren:
# Anwendungsstammverzeichnis
@app.route('/', methods=['GET'])
def index() -> tuple:
# Weiterleitung zu /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:
# Der zur Aktion gehörende Controller wird ausgeführt
return front_controller()
Keine dieser Routen eignet sich zur Initialisierung einer Sitzung jSON oder XML:
- Zeilen 2–5: Die Route [/] initialisiert eine Sitzung HTML;
- Zeilen 8–11: Die Route [/init-session] erfordert ein Token CSRF, das uns nicht bekannt ist;
Wir beschließen, dem Server eine neue Route hinzuzufügen:
# init-session-without-csrftoken
@app.route('/init-session-without-csrftoken/<string:type_response>', methods=['GET'])
def init_session_without_csrftoken(type_response: str) -> tuple:
# Weiterleitung zu /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 Token CSRF. Damit sind wir wieder bei der Route [/init-session] aus der vorherigen Version angelangt;
- Zeilen 4–5: Der Client wird umgeleitet (jSON, XML, HTML) auf die Route [/init-session] umgeleitet, die das Token CSRF in ihren Parametern enthält;
Man kann diese neue Route mit einem Browser ausprobieren:

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

- Bei [1] wurde der Server auf die Route [/init-session] mit dem Token CSRF im URL umgeleitet;
- in [2] befindet sich das Token CSRF im Wörterbuch jSON, das vom Server gesendet wurde und dem Schlüssel [csrf_token] zugeordnet ist;
Kehren wir zum Client-Code zurück:

Wir ändern die Konfiguration [config] wie folgt:
config.update({
# Steuerzahlerdatei
"taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
# Ergebnisdatei
"resultsFilename": f"{script_dir}/../data/output/résultats.json",
# Fehlerdatei
"errorsFilename": f"{script_dir}/../data/output/errors.txt",
# Protokolldatei
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# Steuerberechnungsserver
"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",
}
},
# Debug-Modus
"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-Token arbeitet oder nicht;
- Zeilen 37–40: Wir legen den Dienst-URL für die Aktion [init-session] fest:
- Wenn der Server CSRF-Token verwendet, dann ist der Dienst-URL [/init-session-without-csrftoken];
- ansonsten ist der Dienst-URL der [/init-session];
Die Route [/init-session-without-csrftoken] wurde vorgestellt. Sie ermöglicht es einem Client mit den Tokens jSON / XML, eine Sitzung mit dem Server zu starten, ohne über das Token CSRF zu verfügen. Dieses Token findet er in der Antwort des Servers.
Anschließend ändern wir die Klasse [ImpôtsDaoWithHttpSession], die die Client-Schicht [dao] implementiert:

# Importe
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):
# Konstruktor
def __init__(self, config: dict):
# Initialisierung des übergeordneten Objekts
AbstractImpôtsDao.__init__(self, config)
# Speicherung der Konfigurationselemente
# Allgemeine Konfiguration
self.__config = config
# Server
self.__config_server = config["server"]
# Dienste
self.__config_services = config["server"]['url_services']
# Debug-Modus
self.__debug = config["debug"]
# Protokollierung
self.__logger = None
# Cookies
self.__cookies = None
# Sitzungstyp (json, xml)
self.__session_type = None
# Token CSRF
self.__csrf_token = None
# Request-/Response-Phase
def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
# [method]: Methode HTTP, GET oder POST
# [url_service]: URL als Dienst
# [data]: Parameter des POST im Format „x-www-form-urlencoded“
# [json]: Parameter von POST im JSON-Format
# [cookies]: In die Anfrage einzufügende Cookies
# Es muss eine Sitzung mit XML oder JSON vorhanden sein, andernfalls kann die Antwort nicht verarbeitet werden
if self.__session_type not in ['json', 'xml']:
raise ImpôtsError(73, "il n'y a pas de session valide en cours")
# Das Token CSRF wird zum Service-Token URL hinzugefügt
if self.__csrf_token:
url_service = f"{url_service}/{self.__csrf_token}"
# Ausführung der Anfrage
response = requests.request(method,
url_service,
data=data_value,
json=json_value,
cookies=self.__cookies,
allow_redirects=True)
# Debug-Modus?
if self.__debug:
# Protokollierung
if not self.__logger:
self.__logger = self.__config['logger']
# Protokollierung läuft
self.__logger.write(f"{response.text}\n")
# Ergebnis
if self.__session_type == "json":
résultat = json.loads(response.text)
else: # XML
résultat = xmltodict.parse(response.text[39:])['root']
# Die Cookies aus der Antwort werden abgerufen, falls vorhanden
if response.cookies:
self.__cookies = response.cookies
# das Token wird abgerufen CSRF
if self.__config['with_csrftoken']:
self.__csrf_token = résultat.get('csrf_token', None)
# Statuscode
status_code = response.status_code
# wenn der Statuscode ungleich 200 ist, OK
if status_code != status.HTTP_200_OK:
raise ImpôtsError(35, résultat['réponse'])
# Das Ergebnis wird zurückgegeben
return résultat['réponse']
def init_session(self, session_type: str):
# die Art der Sitzung wird vermerkt
self.__session_type = session_type
# Das Token CSRF aus den vorherigen Aufrufen wird gelöscht
self.__csrf_token = None
# man fordert das URL der Aktion „init-session“ an
url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
# Anfrage wird ausgeführt
self.get_response("GET", url_service)
…
- Zeilen 38–92: Die Verwaltung des Tokens CSRF erfolgt hauptsächlich in der Methode [get_response];
- Zeile 60: Der wichtige Punkt ist der Parameter [allow_redirects=True]. Dies ist sein Standardwert, aber wir wollten ihn besonders hervorheben;
Im Modus [with_csrftoken=True]:
- beginnen die Clients ihren Dialog mit dem Server durch den 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 [requests] ausgeführt;
- Das Token CSRF wird im abgerufenen Ergebnis in den Zeilen 72 und 74 gefunden, zugeordnet zum Schlüssel [csrf_token];
Im Modus [with_csrftoken=False]:
- (Fortsetzung)
- beginnen die Clients ihren Dialog mit dem Server durch den 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 [requests] befolgt;
- In den Zeilen 81–82 gibt es kein Token CSRF, das abgerufen werden könnte. Die Eigenschaft [self.__csrf_token] bleibt daher weiterhin auf „None“ gesetzt (Zeile 36);
- Zeilen 51–52: Bei allen folgenden Anfragen wird das Token CSRF, sofern vorhanden, an die ursprüngliche Route angehängt;
- Zeilen 81–82: Das neue Token, das der Server bei jeder neuen Anfrage des Clients generiert, wird lokal gespeichert, um in Zeile 52 bei der nächsten Anfrage zurückgesendet zu werden;
Außerdem ändert sich die Methode [init_session] geringfügig:
def init_session(self, session_type: str):
# Der Sitzungstyp wird notiert
self.__session_type = session_type
# Das Token CSRF aus den vorherigen Aufrufen wird gelöscht
self.__csrf_token = None
# Anforderung des URL für die Aktion „init-session“
url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
# Abfrage wird ausgeführt
self.get_response("GET", url_service)
Man muss hier bedenken, dass wir eine Route [/init-session-without-csrftoken/<type-response>] erstellt haben, um den Client-Server-Dialog ohne das Token CSRF zu initialisieren. Wir haben jedoch gesehen, dass die in Zeile 12 des Codes aufgerufene Methode [get_response] systematisch das in [self.__csrf_token] gespeicherte Token CSRF an das Ende des Dienst-Tokens URL anhängt. Deshalb wird in Zeile 6 des Codes dieses Token CSRF gelöscht, falls es vorhanden war.
Das war’s. Zu Testzwecken führen wir Folgendes aus:
- die Konsolen-Clients [main, main2, main3];
- die Testklassen [Test1HttpClientDaoWithSession] und [Test2HttpClientDaoWithSession];
wobei der Konfigurationsparameter [with_csrftoken] nacheinander auf „True“ und dann auf „False“ gesetzt wird.

Hier ein Beispiel für die Protokolle, die bei der Ausführung des Clients [main json] mit [with_csrftoken=True] erstellt wurden:
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
Betrachtet man die nacheinander empfangenen Tokens CSRF, so sieht man, dass sie alle unterschiedlich sind.