Skip to content

34. Ćwiczenie praktyczne: wersja 14

Image

Folder [http-servers/09] z wersji 14 powstaje poprzez skopiowanie folderu [http-servers/08] z wersji 13.

34.1. Wprowadzenie

CSRF (Cross Site Request Forgery) to technika kradzieży sesji. Została ona wyjaśniona w Wikipedii (https://fr.wikipedia.org/wiki/Cross-site_request_forgery) w następujący sposób:

Załóżmy, że Alice jest administratorką forum i jest zalogowana do niego za pomocą systemu sesji. Malorie jest członkinią tego samego forum i chce usunąć jeden z postów. Ponieważ nie posiada odpowiednich uprawnień na swoim koncie, wykorzystuje konto Alice za pomocą ataku typu CSRF.
  1. Malorie udaje się zdobyć link umożliwiający usunięcie danego wpisu.
  2. Malorie wysyła do Alice wiadomość zawierającą rzekomy obrazek do wyświetlenia (który w rzeczywistości jest skryptem). Kod URL zawarty w tym obrazku stanowi link do skryptu umożliwiającego usunięcie wybranego wpisu.
  3. Alice musi mieć otwartą sesję w przeglądarce na stronie, na którą celuje Malorie. Jest to warunek konieczny, aby atak przebiegł w sposób niezauważalny, bez konieczności wysyłania żądania uwierzytelnienia, które mogłoby zaalarmować Alice. Sesja ta musi posiadać uprawnienia niezbędne do wykonania destrukcyjnego żądania Malorie. Nie jest konieczne, aby w przeglądarce była otwarta karta z docelową stroną, ani nawet aby przeglądarka była uruchomiona. Wystarczy, że sesja jest aktywna.
  4. Alice czyta wiadomość od Malorie, a jej przeglądarka korzysta z otwartej sesji Alice i nie wymaga interaktywnego uwierzytelniania. Próbuje pobrać zawartość obrazu. W ten sposób przeglądarka uruchamia link i usuwa wiadomość, pobierając tekstową stronę internetową jako zawartość obrazu. Nie rozpoznając powiązanego typu obrazu, nie wyświetla obrazu, a Alice nie wie, że Malorie właśnie sprawiła, iż usunęła wiadomość wbrew swojej woli.

Nawet wyjaśniona w ten sposób technika CSRF jest trudna do zrozumienia. Sporządźmy schemat:

Image

  • w [1-2] Alicja łączy się z forum (strona A). Forum to utrzymuje sesję dla każdego użytkownika. Przeglądarka Alicji przechowuje lokalnie ten plik cookie sesji i odsyła go za każdym razem, gdy wysyła nowe żądanie do strony A;
  • w [3] Malorie wysyła wiadomość do Alicji. Ta czyta ją w swojej przeglądarce. Przeczytana wiadomość ma format HTML i zawiera link do obrazka na stronie B. W rzeczywistości link ten prowadzi do skryptu JavaScript, który uruchamia się po dotarciu do przeglądarki Alicji;
  • skrypt JavaScript wysyła następnie żądanie do strony A. Przeglądarka Alice automatycznie wysyła to żądanie wraz z lokalnie zapisanym plikiem cookie sesji. W tym momencie dochodzi do ataku: Malorie zdołała wysłać żądanie do strony A, korzystając z uprawnień (sesji) Alice. Następnie, niezależnie od dalszego przebiegu wydarzeń, atak został przeprowadzony;

Aby zapobiec tego typu atakom, strona A może postępować w następujący sposób:

  • przy każdej wymianie danych [1-2] z Alicją strona A wysyła klucz, zwany dalej tokenem (żetonem) CSRF, który Alicja musi odesłać przy następnym żądaniu. W ten sposób Alicja musi przy każdym żądaniu przesłać dwie informacje:
    • plik cookie sesji;
    • token CSRF otrzymany w odpowiedzi na jej ostatnie żądanie skierowane do serwisu A;

Na tym polega zabezpieczenie: jeśli przeglądarka automatycznie odsyła do serwisu A plik cookie sesji, nie robi tego w przypadku tokenu CSRF. Z tego powodu wymiana 6-7 przeprowadzona przez skrypt atakujący zostanie odrzucona, ponieważ żądanie 6 nie będzie zawierało tokenu CSRF;

Witryna A może wysłać Alicji token CSRF na różne sposoby dla aplikacji HTML:

  • może przy każdym żądaniu wysyłać stronę o nazwie HTML, na której wszystkie linki będą zawierały token CSRF, na przykład [http://siteA/chemin/csrf_token]. Podczas kolejnego żądania, gdy Alice kliknie jeden z tych linków, strona A będzie musiała jedynie pobrać token CSRF z pola URL w żądaniu i sprawdzić, czy jest on poprawny. Tak właśnie zostanie to zrobione w tym przypadku;
  • w przypadku stron HTML zawierających formularz strona A może wysłać ten formularz wraz z ukrytym polem [input type=’hidden’] zawierającym token CSRF. Token ten zostanie wówczas automatycznie przesłany wraz z formularzem, gdy Alice zatwierdzi stronę. Witryna A pobierze token CSRF z treści (body) żądania;
  • możliwe są również inne techniki;

34.2. Konfiguracja

Image

W konfiguracji aplikacji [parameters] wprowadzamy dwie wartości logiczne:

  • [with_redissession]: przy wartości True aplikacja korzysta z sesji Redis. Przy wartości False aplikacja korzysta ze zwykłej sesji Flask;
  • [with_csrftoken]: przy wartości True parametry URL aplikacji zawierają token CSRF;

        # czas przerwy wątku w sekundach
        "sleep_time"0,
        # serwer Redis
        "with_redissession"True,
        "redis": {
            "host""127.0.0.1",
            "port"6379
        },
        # token CSRF
        "with_csrftoken"False,

34.3. Implementacja CSRF

Zapewnimy, że gdy:


config['parameters']['with_csrftoken']

ma wartość [True], aplikacja wysyła do przeglądarki klienta strony internetowe, których linki będą zawierały token CSRF.

34.3.1. Moduł [flask_wtf]

Implementacja tokenu CSRF zostanie przeprowadzona za pomocą modułu [flask_wtf], który instalujemy w terminalu PyCharm:


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

34.3.2. Szablony widoków

Wprowadzamy nową klasę do szablonów:

Image

Klasa [AbstractBaseModelForView] ma następującą postać:


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 ""
  • wiersz 9: klasa [AbstractBaseModelForView] implementuje interfejs [InterfaceModelForView], który jest implementowany przez klasy szablonów;
  • wiersze 11–13: metoda [get_model_for_view] nie jest zaimplementowana;
  • wiersze 15–20: metoda [get_csrftoken] generuje token CSRF, jeśli aplikacja została skonfigurowana do korzystania z nich. W zależności od sytuacji funkcja zwraca token poprzedzony znakiem /, w przeciwnym razie – pusty ciąg znaków. Cechą charakterystyczną funkcji [generate_csrf] jest to, że zawsze generuje tę samą wartość dla danego żądania klienta. Przetwarzanie żądania wiąże się z wykonaniem różnych funkcji. Użycie funkcji [generate_csrf] w tych funkcjach zawsze generuje tę samą wartość. Natomiast przy kolejnym żądaniu generowany jest nowy token CSRF;

Wszystkie szablony M widoku V będą zawierały token CSRF w następujący sposób:


class ModelForAuthentificationView(AbstractBaseModelForView):

    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
        # dane strony są umieszczane w szablonie
        modèle = {}
        

        # token CSRF
        modèle['csrf_token'] = super().get_csrftoken(config)

        # zwracamy szablon
        return modèle
  • każda klasa szablonu dziedziczy po klasie bazowej [AbstractBaseModelForView];
  • wiersz 8: token CSRF jest pobierany z klasy nadrzędnej. Otrzymujemy albo pusty ciąg znaków, albo ciąg znaków typu [/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c];

34.3.3. Widoki

Z tego, co właśnie widzieliśmy, wszystkie widoki V będą miały w swoim modelu M token CSRF. Będą więc mogły go używać w zawartych w nich linkach. Rozważmy kilka przykładów:

Fragment uwierzytelniający [v_authentification.html]


<!-- formularz HTML – wysyłamy jego wartości za pomocą akcji [authentifier-utilisateur] -->
<form method="post" action="/authentifier-utilisateur{{modèle.csrf_token}}">

    <!-- tytuł -->
    <div class="alert alert-primary" role="alert">
        <h4>Veuillez vous authentifier</h4>
    </div>


</form>
  • wiersz 2: zgodnie z tym, co właśnie omówiliśmy, URL z atrybutu [action] będzie wyglądał następująco:

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

lub

[/authentifier-utilisateur]

w zależności od tego, czy aplikacja została skonfigurowana do korzystania z tokenów CSRF;

Fragment obliczeń podatku [v-calcul-impot.html]


<!-- formularz HTML wysłany -->
<form method="post" action="/calculer-impot{{modèle.csrf_token}}">
    <!-- wiadomość w 12 kolumnach na niebieskim tle -->
    <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>

Fragment symulacji [v-liste-simulations.html]


{% if modèle.simulations is undefined or modèle.simulations|length==0 %}
<!-- komunikat na niebieskim tle -->
<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 %}
<!-- komunikat na niebieskim tle -->
<div class="alert alert-primary" role="alert">
    <h4>Liste de vos simulations</h4>
</div>

<!-- tabela symulacji -->
<table class="table table-sm table-hover table-striped">
    
    <!-- treść tabeli (wyświetlane dane) -->
    <tbody>
    <!-- wyświetlanie poszczególnych symulacji poprzez przeglądanie tabeli symulacji -->
    {% for simulation in modèle.simulations %}

    <!-- wyświetlanie wiersza tabeli z 6 kolumnami – tag <tr> -->
    <!-- kolumna 1: nagłówek wiersza (nr symulacji) – tag <th scope='row' -->
    <!-- kolumna 2: wartość parametru [marié] – tag <td> -->
    <!-- kolumna 3: wartość parametru [enfants] – tag <td> -->
    <!-- kolumna 4: wartość parametru [salaire] – tag <td> -->
    <!-- kolumna 5: wartość parametru [impôt] (podatku) – tag <td> -->
    <!-- kolumna 6: wartość parametru [surcôte] – tag <td> -->
    <!-- kolumna 7: wartość parametru [décôte] – tag <td> -->
    <!-- kolumna 8: wartość parametru [réduction] – tag <td> -->
    <!-- kolumna 9: wartość parametru [taux] (podatku) – tag <td> -->
    <!-- kolumna 10: link do usunięcia symulacji – 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 %}

Fragment menu [v-menu.html]


<!-- menu Bootstrap -->
<nav class="nav flex-column">
    <!-- wyświetlanie listy linków 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. Trasy

Obecnie istnieją dwa rodzaje tras, w zależności od tego, czy wykorzystują one token, czy nie: CSRF:

Image

  • [routes_without_csrftoken] to trasy bez tokenu CSRF. Są to trasy z poprzedniej wersji;
  • [routes_with_csrftoken] to trasy z tokenem CSRF.

W [routes_with_csrftoken] trasy mają teraz dodatkowy parametr – token CSRF:


# kontroler front-end
def front_controller() -> tuple:
    # przekazujemy żądanie do kontrolera głównego
    main_controller = config['mvc']['controllers']['main-controller']
    return main_controller.execute(request, session, config)

@app.route('/', methods=['GET'])
def index() -> tuple:
    # przekierowanie do /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:
    # uruchamiany jest kontroler powiązany z akcją
    return front_controller()

# uwierzytelnianie użytkownika
@app.route('/authentifier-utilisateur/<string:csrf_token>', methods=['POST'])
def authentifier_utilisateur(csrf_token: str) -> tuple:
    # uruchamiany jest kontroler powiązany z akcją
    return front_controller()

# oblicz-podatek
@app.route('/calculer-impot/<string:csrf_token>', methods=['POST'])
def calculer_impot(csrf_token: str) -> tuple:
    # uruchamiany jest kontroler powiązany z akcją
    return front_controller()

# obliczanie podatku partiami
@app.route('/calculer-impots/<string:csrf_token>', methods=['POST'])
def calculer_impots(csrf_token: str):
    # uruchamia się kontroler powiązany z akcją
    return front_controller()

# wyświetlanie listy symulacji
@app.route('/lister-simulations/<string:csrf_token>', methods=['GET'])
def lister_simulations(csrf_token: str) -> tuple:
    # uruchamiany jest kontroler powiązany z akcją
    return front_controller()

# usuń-symulację
@app.route('/supprimer-simulation/<int:numero>/<string:csrf_token>', methods=['GET'])
def supprimer_simulation(numero: int, csrf_token: str) -> tuple:
    # uruchamia się kontroler powiązany z akcją
    return front_controller()

# koniec sesji
@app.route('/fin-session/<string:csrf_token>', methods=['GET'])
def fin_session(csrf_token: str) -> tuple:
    # uruchamia się kontroler powiązany z akcją
    return front_controller()

# wyświetl-obliczenia-podatku
@app.route('/afficher-calcul-impot/<string:csrf_token>', methods=['GET'])
def afficher_calcul_impot(csrf_token: str) -> tuple:
    # uruchamiany jest kontroler powiązany z akcją
    return front_controller()

# pobierz-dane-administracyjne
@app.route('/get-admindata/<string:csrf_token>', methods=['GET'])
def get_admindata(csrf_token: str) -> tuple:
    # uruchamia się kontroler powiązany z akcją
    return front_controller()

Wszystkie trasy mają teraz w swoich parametrach token CSRF, nawet trasa [/init-session]. Oznacza to, że klient nie może uruchomić aplikacji, wpisując bezpośrednio URL [/init-session/html], ponieważ zabraknie tokenu CSRF. Teraz musi koniecznie przejść przez ścieżki URL i [/] z wierszy 7–10.

Wybór tras odbywa się w głównym skrypcie [main]:



# główny wątek nie potrzebuje już modułu logującego
logger.close()

# jeśli wystąpił błąd, proces zostaje zatrzymany
if erreur:
    sys.exit(2)

# import tras aplikacji internetowej
if config['parameters']['with_csrftoken']:
    import routes_with_csrftoken as routes
else:
    import routes_without_csrftoken as routes

# konfiguracja tras
routes.config = config

# uruchomienie aplikacji Flask
routes.execute(__name__)
  • wiersze 9–13: wybór tras w zależności od tego, czy aplikacja korzysta z tokenów CSRF, czy nie;

34.3.5. Kontroler [MainController]

Przy każdym żądaniu serwer musi sprawdzić obecność tokenu CSRF. Zrobimy to w głównym kontrolerze [MainController], przez który przechodzą wszystkie żądania:


from flask_wtf.csrf import generate_csrf, validate_csrf

       # przetwarzanie żądania
        try:
            # rejestrowanie
            logger = Logger(config['parameters']['logsFilename'])

            …

            # pobieranie elementów ścieżki
            params = request.path.split('/')

            # akcja jest pierwszym elementem
            action = params[1]

            …

            if config['parameters']['with_csrftoken']:
                # csrf_token jest ostatnim elementem ścieżki
                csrf_token = params.pop()
                # sprawdzana jest poprawność tokenu
                # zostanie zgłoszony wyjątek, jeśli csrf_token nie jest tokenem oczekiwanym
                validate_csrf(csrf_token)

            …

        except ValidationError as exception:
            # nieważny token CSRF
            résultat = {"action": action, "état"121"réponse"[f"{exception}"]}
            status_code = status.HTTP_400_BAD_REQUEST

        except BaseException as exception:
            # inne (nieoczekiwane) wyjątki
            résultat = {"action": action, "état"131"réponse"[f"{exception}"]}
            status_code = status.HTTP_400_BAD_REQUEST

        finally:
            pass

        # do wyniku dodawany jest csrf_token
        résultat['csrf_token'] = generate_csrf()

        # rezultat wysłany do klienta jest rejestrowany
        log = f"[MainController] {résultat}\n"
        logger.write(log)
  • wiersz 20: pobieramy token CSRF z URL zawartego w żądaniu typu [http://machine :port/chemin/action/param1/param2/…/csrf_token]. Token sesji jest zawsze ostatnim elementem URL;
  • wiersz 23: sprawdzana jest ważność tokenu CSRF pobranego z URL w połączeniu z tokenem CSRF z sesji. Jeśli token jest nieprawidłowy, funkcja [validate_csrf] zgłasza wyjątek typu [ValidationError] (wiersz 27);
  • wiersz 41: token CSRF jest umieszczany w wyniku wysyłanym do klienta. Klienci jSON i XML będą go potrzebować. Klienci ci nie otrzymują bowiem stron HTML z tokenem CSRF w linkach zawartych na tych stronach. Otrzymają go zatem w wyniku jSON lub XML wysłanym przez serwer;

Uwaga: funkcja [validate_csrf] w wierszu 23 nie sprawdza ścisłej zgodności. Token CSRF jest zapisywany w sesji wraz z kluczem [csrf_token]. Testy wydają się wskazywać, że token CSRF jest ważny, jeśli został wygenerowany w trakcie sesji. Jeśli więc ręcznie, na przykład w adresie URL wyświetlanym w przeglądarce (/lister-simulations/xyz), zastąpisz token [xyz] lub CSRF innym tokenem, np. [abc], otrzymanym już podczas poprzedniej akcji, akcja [/lister-simulations] zakończy się powodzeniem;

34.4. Testy w przeglądarce

W:

  • uruchamia serwer z parametrem od [with_csrftoken] do [True];
  • wywołujemy URL i [http://localhost:5000] za pomocą przeglądarki;

Image

  • w [1] token CSRF;

Wykonajmy te operacje, aż uzyskamy listę symulacji:

Image

Teraz ręcznie wpiszmy URL [http://localhost:5000/supprimer-simulation/1/x], aby usunąć symulację o id=1. Celowo podajemy nieprawidłowy token CSRF, aby zobaczyć, co się stanie. Odpowiedź serwera jest następująca:

Image

Uwaga 1: nie ma pewności, czy zastosowana tutaj metoda zawsze wystarczy, by przeciwdziałać atakom typu CSRF. Wróćmy do schematu ataku:

Image

Jeśli skrypt JavaScript pobrany jako [5] jest w stanie odczytać historię przeglądarki używanej przez Alicję, będzie w stanie odzyskać adresy URL wykonane przez przeglądarkę, a także adresy URL, takie jak [/cible/csrf_token]. Będzie mógł wówczas odzyskać token sesji [csrf_token] i przeprowadzić atak w [6-7]. Niemniej jednak przeglądarka zezwala wyłącznie na wykorzystanie historii okna przeglądarki, w którym skrypt jest uruchamiany. Jeśli więc Alice nie korzysta z tego samego okna do pracy ze stroną A o adresie [1-2] i do czytania wiadomości od Malorie o adresie [3], atak o adresie CSRF nie będzie możliwy.

34.5. Klienci konsoli

Innym sposobem przetestowania wersji 14 aplikacji jest wykorzystanie testów z wersji 12 i dostosowanie ich do nowego serwera.

Image

Folder [impots/http-clients/09] uzyskuje się początkowo poprzez skopiowanie folderu [impots/http-clients/07]. Następnie jest on modyfikowany.

Wróćmy do ścieżek inicjujących sesję:


# katalog główny aplikacji
@app.route('/', methods=['GET'])
def index() -> tuple:
    # przekierowanie do /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:
    # uruchamiany jest kontroler powiązany z akcją
    return front_controller()

Żadna z tych tras nie nadaje się do zainicjowania sesji jSON ani XML:

  • wiersze 2–5: trasa [/] inicjuje sesję HTML;
  • wiersze 8–11: trasa [/init-session] wymaga tokenu CSRF, którego nie znamy;

Postanawiamy dodać nową trasę do serwera:


# init-session-without-csrftoken
@app.route('/init-session-without-csrftoken/<string:type_response>', methods=['GET'])
def init_session_without_csrftoken(type_response: str) -> tuple:
    # przekierowanie do /init-session/type_response
    return redirect(url_for("init_session", type_response=type_response, csrf_token=generate_csrf()), status.HTTP_302_FOUND)
  • wiersz 2: nowa trasa. Nie wymaga ona tokenu CSRF. W ten sposób powróciliśmy do trasy [/init-session] z poprzedniej wersji;
  • wiersze 4–5: przekierowujemy klienta (jSON, XML, HTML) na trasę [/init-session], która ma w swoich parametrach token CSRF;

Można wypróbować tę nową trasę w przeglądarce:

Image

Odpowiedź serwera (skonfigurowanego z [with_csrftoken=True]) jest następująca:

Image

  • w przypadku [1] serwer został przekierowany na trasę [/init-session] z tokenem CSRF w URL;
  • w [2] token CSRF znajduje się w słowniku jSON wysłanym przez serwer powiązany z kluczem [csrf_token];

Wróćmy do kodu klienta:

Image

Modyfikujemy konfigurację [config] w następujący sposób:


   config.update({
        # plik podatników
        "taxpayersFilename"f"{script_dir}/../data/input/taxpayersdata.txt",
        # plik wyników
        "resultsFilename"f"{script_dir}/../data/output/résultats.json",
        # plik błędów
        "errorsFilename"f"{script_dir}/../data/output/errors.txt",
        # plik logów
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # serwer obliczający podatek
        "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",
            }
        },
        # tryb debugowania
        "debug"True,
        # csrf_token
        "with_csrftoken"True,
    }
    )

    # trasa 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'
  • wiersz 31: zmienna logiczna wskaże klientowi, czy serwer, do którego się zwraca, obsługuje tokeny CSRF;
  • wiersze 37–40: ustalamy URL usługi dla akcji [init-session]:
    • jeśli serwer korzysta z tokenów CSRF, wówczas token usługi URL to [/init-session-without-csrftoken];
    • w przeciwnym razie identyfikator usługi URL to [/init-session];

Przedstawiono trasę [/init-session-without-csrftoken]. Umożliwia ona klientowi o tokenie jSON / XML rozpoczęcie sesji z serwerem bez posiadania tokenu CSRF. Token ten znajdzie on w odpowiedzi serwera.

Następnie modyfikujemy klasę [ImpôtsDaoWithHttpSession], która implementuje warstwę klienta [dao]:

Image


# importy
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):
        # inicjalizacja elementu nadrzędnego
        AbstractImpôtsDao.__init__(self, config)
        # zapisywanie elementów konfiguracji
        # ogólna konfiguracja
        self.__config = config
        # serwer
        self.__config_server = config["server"]
        # usługi
        self.__config_services = config["server"]['url_services']
        # tryb debugowania
        self.__debug = config["debug"]
        # rejestrator
        self.__logger = None
        # pliki cookie
        self.__cookies = None
        # typ sesji (json, xml)
        self.__session_type = None
        # token CSRF
        self.__csrf_token = None

    # etap żądania / odpowiedzi
    def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
        # [method]: metoda HTTP, GET lub POST
        # [url_service]: URL serwisowe
        # [data]: parametry POST w formacie x-www-form-urlencoded
        # [json]: parametry POST w formacie JSON
        # [cookies]: pliki cookie, które należy dołączyć do żądania

        # należy posiadać sesję XML lub JSON, w przeciwnym razie nie będzie można obsłużyć odpowiedzi
        if self.__session_type not in ['json''xml']:
            raise ImpôtsError(73"il n'y a pas de session valide en cours")

        # do żądania serwisowego URL dodaje się token CSRF
        if self.__csrf_token:
            url_service = f"{url_service}/{self.__csrf_token}"

        # wykonanie żądania
        response = requests.request(method,
                                    url_service,
                                    data=data_value,
                                    json=json_value,
                                    cookies=self.__cookies,
                                    allow_redirects=True)

        # tryb debugowania?
        if self.__debug:
            # moduł logowania
            if not self.__logger:
                self.__logger = self.__config['logger']
            # rejestruje się
            self.__logger.write(f"{response.text}\n")

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

        # pobieramy pliki cookie z odpowiedzi, jeśli takie istnieją
        if response.cookies:
            self.__cookies = response.cookies

        # pobieramy token CSRF
        if self.__config['with_csrftoken']:
            self.__csrf_token = résultat.get('csrf_token'None)

        # kod statusu
        status_code = response.status_code

        # jeśli kod statusu jest inny niż 200, to OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(35, résultat['réponse'])

        # zwracamy wynik
        return résultat['réponse']

    
    def init_session(self, session_type: str):
        # odnotowuje się typ sesji
        self.__session_type = session_type

        # usuwamy token CSRF z poprzednich wywołań
        self.__csrf_token = None

        # żądamy URL dla akcji init-session
        url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        # wykonanie żądania
        self.get_response("GET", url_service)

  • wiersze 38–92: obsługa tokenu CSRF odbywa się głównie w metodzie [get_response];
  • wiersz 60: istotnym elementem jest parametr [allow_redirects=True]. Jest to jego wartość domyślna, ale postanowiliśmy ją podkreślić;

W trybie [with_csrftoken=True]:

  • klienci rozpoczynają komunikację z serwerem od wywołania trasy [/init-session_without_csftoken/type_response];
  • serwer odpowiada na to żądanie przekierowaniem do trasy [/init-session/type_response/csrf_token];
  • z powodu parametru [allow_redirects=True] przekierowanie to zostanie zrealizowane przez klienta [requests];
  • token CSRF zostanie znaleziony w pobranym wyniku w wierszach 72 i 74, powiązany z kluczem [csrf_token];

W trybie [with_csrftoken=False]:

  • (ciąg dalszy)
    • klienci rozpoczynają dialog z serwerem od wywołania trasy [/init-session /type_response];
    • serwer odpowiada na to żądanie przekierowaniem do trasy [/init-session/type_response];
    • z powodu parametru [allow_redirects=True] przekierowanie to zostanie zrealizowane przez klienta [requests];
    • w wierszach 81–82 nie ma tokenu CSRF do pobrania. Właściwość [self.__csrf_token] pozostaje zatem nadal ustawiona na None (wiersz 36);
  • wiersze 51–52: w przypadku wszystkich kolejnych żądań token CSRF, o ile istnieje, jest dodawany do początkowej trasy;
  • wiersze 81–82: nowy token generowany przez serwer przy każdym nowym żądaniu klienta jest zapisywany lokalnie, aby zostać odesłany w wierszu 52 przy następnym żądaniu;

Ponadto metoda [init_session] ulega niewielkiej zmianie:


    def init_session(self, session_type: str):
        # odnotowuje się typ sesji
        self.__session_type = session_type

        # usuwany jest token CSRF z poprzednich wywołań
        self.__csrf_token = None

        # żądanie URL dla akcji init-session
        url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        # wykonanie żądania
        self.get_response("GET", url_service)

Należy tutaj pamiętać, że utworzono trasę [/init-session-without-csrftoken/<type-response>] w celu zainicjowania dialogu klient–serwer bez tokenu CSRF. Widzieliśmy jednak, że metoda [get_response], wywoływana w linii 12 kodu, systematycznie dodaje na końcu kodu usługi URL token CSRF zapisany w [self.__csrf_token]. Dlatego w wierszu 6 kodu usuwa się ten token CSRF, jeśli istniał.

To wszystko. W celu przeprowadzenia testów należy wykonać:

  • klienty konsoli [main, main2, main3];
  • klasy testowe [Test1HttpClientDaoWithSession] i [Test2HttpClientDaoWithSession];

ustawiając kolejno na True, a następnie na False parametr konfiguracyjny [with_csrftoken].

Image

Oto przykładowe logi uzyskane podczas uruchomienia klienta [main json] wraz z [with_csrftoken=True]:


2020-08-08 16:33:23.317903, MainThread : début du calcul de l'impôt des contribuables
2020-08-08 16:33:23.317903, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-08-08 16:33:23.317903, Thread-2 : début du calcul de l'impôt des 2 contribuables
2020-08-08 16:33:23.317903, Thread-3 : début du calcul de l'impôt des 4 contribuables
2020-08-08 16:33:23.317903, Thread-4 : début du calcul de l'impôt des 1 contribuables
2020-08-08 16:33:23.379221, Thread-2 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"], "csrf_token": "ImFiZmZkYjZmMzFkZDc2YWRjNWYwOGM0NTBmMGM4ODJjYzViOWI4NGEi.Xy63sw.H5L0--yWsvfaWvggrGw78z5VnN0"}
2020-08-08 16:33:23.381073, Thread-4 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"], "csrf_token": "ImY5YzQyMjlkYzcyYmM4YmZiMGI0NWY5MjE4MzIzNDExZjc0MGQ3MWQi.Xy63sw.q6olg7IP_g2ro_RBFRCX1BX90g8"}
2020-08-08 16:33:23.386982, Thread-3 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"], "csrf_token": "IjkxZGNlN2YyMmUxMjQ0M2Y0MTdjNDQ4ZmQ1MDMxZjkwNjBhNzAzZjMi.Xy63sw.-6buL11No3UJBlElpW4tX4B-lp0"}
2020-08-08 16:33:23.390269, Thread-1 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"], "csrf_token": "IjIxNmU4MDQyZDFmZmIyZDlmZjE4MzNlNDUzYzFjMGYxMWYxYzEwNGYi.Xy63sw.fgs6Cm2owsJf4NjTm7gKrVESabI"}
2020-08-08 16:33:23.413206, Thread-2 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie", "csrf_token": "ImFiZmZkYjZmMzFkZDc2YWRjNWYwOGM0NTBmMGM4ODJjYzViOWI4NGEi.Xy63sw.H5L0--yWsvfaWvggrGw78z5VnN0"}
2020-08-08 16:33:23.422877, Thread-2 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 2}], "csrf_token": "ImFiZmZkYjZmMzFkZDc2YWRjNWYwOGM0NTBmMGM4ODJjYzViOWI4NGEi.Xy63sw.H5L0--yWsvfaWvggrGw78z5VnN0"}
2020-08-08 16:33:23.428622, Thread-4 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie", "csrf_token": "ImY5YzQyMjlkYzcyYmM4YmZiMGI0NWY5MjE4MzIzNDExZjc0MGQ3MWQi.Xy63sw.q6olg7IP_g2ro_RBFRCX1BX90g8"}
2020-08-08 16:33:23.429127, Thread-3 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie", "csrf_token": "IjkxZGNlN2YyMmUxMjQ0M2Y0MTdjNDQ4ZmQ1MDMxZjkwNjBhNzAzZjMi.Xy63sw.-6buL11No3UJBlElpW4tX4B-lp0"}
2020-08-08 16:33:23.429127, Thread-1 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie", "csrf_token": "IjIxNmU4MDQyZDFmZmIyZDlmZjE4MzNlNDUzYzFjMGYxMWYxYzEwNGYi.Xy63sw.fgs6Cm2owsJf4NjTm7gKrVESabI"}
2020-08-08 16:33:23.429127, Thread-2 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée", "csrf_token": "IjU1YjlmZDA0OWRhNTJlODFmYjgyYjlhM2ExYWNhZmUzNTk2NjA5NGIi.Xy63sw.nyNSvkcG6iG0oIMBjtYPo8ySgdw"}
2020-08-08 16:33:23.438519, Thread-2 : fin du calcul de l'impôt des 2 contribuables
2020-08-08 16:33:23.443033, Thread-4 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 1}], "csrf_token": "ImY5YzQyMjlkYzcyYmM4YmZiMGI0NWY5MjE4MzIzNDExZjc0MGQ3MWQi.Xy63sw.q6olg7IP_g2ro_RBFRCX1BX90g8"}
2020-08-08 16:33:23.446510, Thread-3 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 4}], "csrf_token": "IjkxZGNlN2YyMmUxMjQ0M2Y0MTdjNDQ4ZmQ1MDMxZjkwNjBhNzAzZjMi.Xy63sw.-6buL11No3UJBlElpW4tX4B-lp0"}
2020-08-08 16:33:23.453477, Thread-1 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}], "csrf_token": "IjIxNmU4MDQyZDFmZmIyZDlmZjE4MzNlNDUzYzFjMGYxMWYxYzEwNGYi.Xy63sw.fgs6Cm2owsJf4NjTm7gKrVESabI"}
2020-08-08 16:33:23.457912, Thread-4 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée", "csrf_token": "IjQ0ZDQxODgzN2M5NjRiYWI0NjA2MTk5YWFkNGFhMzY1M2IxNWMyNDIi.Xy63sw.mOa5MKXvJ-EXf_qEok-OqC5j_mg"}
2020-08-08 16:33:23.458442, Thread-4 : fin du calcul de l'impôt des 1 contribuables
2020-08-08 16:33:23.459045, Thread-3 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée", "csrf_token": "ImQ0NDZlYmViYjY1ZDUxYzJhMTNmM2JiZTRkMjBjZGJkYzE0OGVkYzMi.Xy63sw.fviTJz4zFDqVLlVlkrosT_JRPww"}
2020-08-08 16:33:23.459700, Thread-3 : fin du calcul de l'impôt des 4 contribuables
2020-08-08 16:33:23.460492, Thread-1 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée", "csrf_token": "Ijg3MjQ1NGUyYTUyOGEyNTdmZmNmYWZkMmU2OTgyMzUwNjI1YTlhZjIi.Xy63sw.I0xBl9Q8DzsuXPSgOdeARc_VKBA"}
2020-08-08 16:33:23.460492, Thread-1 : fin du calcul de l'impôt des 4 contribuables
2020-08-08 16:33:23.460492, MainThread : fin du calcul de l'impôt des contribuables

Jeśli przyjrzymy się kolejno otrzymanym tokenom CSRF, zauważymy, że wszystkie są różne.