Skip to content

34. Anwendungsaufgabe: Version 14

Image

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:

Nehmen wir an, Alice ist Administratorin eines Forums und über ein Sitzungssystem dort angemeldet. Malorie ist Mitglied desselben Forums und möchte einen der Beiträge im Forum löschen. Da sie mit ihrem Konto nicht über die erforderlichen Rechte verfügt, nutzt sie mithilfe eines Angriffs vom Typ CSRF das Konto von Alice.
  1. Malorie gelingt es, den Link herauszufinden, über den sich der betreffende Beitrag löschen lässt.
  2. 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.
  3. 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.
  4. 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:

Image

  • 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

Image

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:

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

[/authentifier-utilisateur]

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:

Image

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

Image

  • in [1] das Token CSRF;

Führen wir einige Bearbeitungen durch, bis wir eine Liste von Simulationen erhalten:

Image

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:

Image

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:

Image

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.

Image

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:

Image

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

Image

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

Image

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:

Image


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

Image

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.