Skip to content

34. Практичне завдання: версія 14

Image

Папка [http-servers/09] версії 14 отримана шляхом копіювання папки [http-servers/08] версії 13.

34.1. Вступ

CSRF (Cross Site Request Forgery) — це техніка викрадення сеансу. У Вікіпедії (https://fr.wikipedia.org/wiki/Cross-site_request_forgery) її пояснюють так:

Припустимо, що Аліса є адміністратором форуму і ввійшла в систему за допомогою механізму сеансів. Малорі — учасниця цього ж форуму, вона хоче видалити одне з повідомлень на форумі. Оскільки її обліковий запис не має необхідних прав, вона використовує обліковий запис Аліси за допомогою атаки типу CSRF.
  1. Малорі вдається дізнатися посилання, за допомогою якого можна видалити відповідне повідомлення.
  2. Малорі надсилає Алісі повідомлення, що містить псевдозображення для перегляду (яке насправді є скриптом). URL у цьому зображенні — це посилання на скрипт, що дозволяє видалити потрібне повідомлення.
  3. Аліса повинна мати відкриту сесію в своєму браузері на сайті, на який націлена Малорі. Це необхідна умова для того, щоб атака пройшла непомітно, без запиту на автентифікацію, який би попередив Алісу. Ця сесія повинна мати необхідні права для виконання руйнівного запиту Малорі. Не обов’язково, щоб у браузері була відкрита вкладка з цільовим сайтом або навіть щоб браузер був запущений. Достатньо, щоб сесія була активною.
  4. Аліса читає повідомлення від Малорі; її браузер використовує відкриту сесію Аліси і не вимагає інтерактивної автентифікації. Він намагається завантажити вміст зображення. При цьому браузер активує посилання та видаляє повідомлення, а як вміст зображення завантажує текстову веб-сторінку. Не розпізнавши відповідний тип зображення, він не відображає зображення, і Аліса не знає, що Малорі щойно змусила її видалити повідомлення всупереч її волі.

Навіть з таким поясненням техніку CSRF важко зрозуміти. Складімо схему:

Image

  • у форматі [1-2] Аліса зв’язується з форумом (сайт А). Цей форум підтримує сесію для кожного користувача. Браузер Аліси зберігає цей сесійний файл cookie локально і надсилає його щоразу, коли надсилає новий запит на сайт А;
  • у [3] Малорі надсилає повідомлення Алісі. Та читає його у своєму браузері. Прочитане повідомлення має формат HTML і містить посилання на зображення з сайту B. Насправді це посилання веде до скрипта JavaScript, який виконується, щойно потрапляє до браузера Аліси;
  • цей скрипт JavaScript надсилає запит до сайту A. Браузер Аліси автоматично надсилає цей запит із сесійним файлом cookie, збереженим локально. Саме тут і відбувається атака: Малорі вдалося надіслати запит до сайту A, використовуючи права (сесію) Аліси. Після цього, незалежно від подальшого розвитку подій, атака вважається здійсненою;

Щоб протидіяти такому типу атаки, сайт A може вчинити наступним чином:

  • під час кожного обміну [1-2] з Алісою сайт A надсилає ключ, який надалі називатимемо токеном (token) CSRF, який Аліса повинна надіслати у відповідь під час наступного запиту. Таким чином, Аліса повинна надсилати з кожним запитом дві інформації:
    • сесійний файл cookie;
    • токен CSRF, отриманий у відповіді на її останній запит до сайту A;

У цьому полягає захист: хоча браузер автоматично надсилає сайту A сесійний файл cookie, він не робить цього з токеном CSRF. З цієї причини обмін 6-7, здійснений скриптом атаки, буде відхилено, оскільки у запиті 6 не буде надіслано токен CSRF;

Сайт A може надіслати Алісі токен CSRF різними способами для додатка HTML:

  • він може з кожним запитом надсилати сторінку HTML, на якій усі посилання міститимуть токен CSRF, наприклад [http://siteA/chemin/csrf_token]. Під час наступного запиту, коли Аліса натисне на одне з цих посилань, сайту A достатньо буде отримати токен CSRF із параметра URL запиту та перевірити його правильність. Саме це й буде зроблено в даному випадку;
  • для сторінок HTML, що містять форму, він може надіслати її разом із прихованим полем [input type=’hidden’], яке містить токен CSRF. Цей токен буде автоматично надіслано разом із формою, коли Аліса підтвердить сторінку. Сайт A отримає токен CSRF у тілі (body) запиту;
  • можливі й інші методи;

34.2. Налаштування

Image

У конфігурацію [parameters] додатка ми вводимо два логічні параметри:

  • [with_redissession]: при значенні True додаток використовує сесію Redis. При значенні False додаток використовує звичайну сесію Flask;
  • [with_csrftoken]: при значенні True URL додатка містять токен CSRF;

        # тривалість паузи потоку в секундах
        "sleep_time"0,
        # сервер Redis
        "with_redissession"True,
        "redis": {
            "host""127.0.0.1",
            "port"6379
        },
        # токен CSRF
        "with_csrftoken"False,

34.3. Реалізація CSRF

Ми забезпечимо, щоб коли:


config['parameters']['with_csrftoken']

дорівнює [True], додаток надсилав клієнтському браузеру веб-сторінки, посилання на які міститимуть токен CSRF.

34.3.1. Модуль [flask_wtf]

Реалізація токена CSRF буде здійснена за допомогою модуля [flask_wtf], який ми встановлюємо в терміналі PyCharm:


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

34.3.2. Шаблони переглядів

Ми вводимо новий клас у шаблони:

Image

Клас [AbstractBaseModelForView] має такий вигляд:


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 ""
  • рядок 9: клас [AbstractBaseModelForView] реалізує інтерфейс [InterfaceModelForView], який реалізують класи шаблонів;
  • рядки 11–13: метод [get_model_for_view] не реалізовано;
  • рядки 15–20: метод [get_csrftoken] генерує токен CSRF, якщо додаток налаштовано на їх використання. Залежно від обставин, функція повертає токен, перед яким стоїть знак /, або порожній рядок. Особливістю функції [generate_csrf] є те, що вона завжди генерує одне й те саме значення для заданого запиту клієнта. Обробка запиту передбачає виконання різних функцій. Використання [generate_csrf] у цих функціях завжди генерує одне й те саме значення. Натомість під час наступного запиту генерується новий токен CSRF;

Усі шаблони M для подання V будуть містити токен CSRF у такий спосіб:


class ModelForAuthentificationView(AbstractBaseModelForView):

    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
        # дані сторінки інкапсулюються в шаблон
        modèle = {}
        

        # токен CSRF
        modèle['csrf_token'] = super().get_csrftoken(config)

        # повертаємо шаблон
        return modèle
  • кожен клас шаблону успадковує базовий клас [AbstractBaseModelForView];
  • рядок 8: у батьківського класу запитується токен CSRF. Отримується або порожній рядок, або рядок типу [/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c];

34.3.3. Види

З того, що ми щойно побачили, усі перегляди V матимуть у своїй моделі M токен CSRF. Отже, вони зможуть використовувати його в посиланнях, які містять. Розглянемо кілька прикладів:

Фрагмент автентифікації [v_authentification.html]


<!-- форма HTML — відправляємо її значення за допомогою дії [authentifier-utilisateur] -->
<form method="post" action="/authentifier-utilisateur{{modèle.csrf_token}}">

    <!-- заголовок -->
    <div class="alert alert-primary" role="alert">
        <h4>Veuillez vous authentifier</h4>
    </div>


</form>
  • рядок 2: згідно з тим, що ми щойно розглянули, URL атрибута [action] буде:

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

або

[/authentifier-utilisateur]

залежно від того, чи налаштовано додаток на використання токенів CSRF;

Фрагмент розрахунку податку [v-calcul-impot.html]


<!-- форма HTML, надіслана -->
<form method="post" action="/calculer-impot{{modèle.csrf_token}}">
    <!-- повідомлення у 12 стовпчиках на синьому тлі -->
    <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>

Фрагмент моделювання [v-liste-simulations.html]


{% if modèle.simulations is undefined or modèle.simulations|length==0 %}
<!-- повідомлення на синьому тлі -->
<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 %}
<!-- повідомлення на синьому тлі -->
<div class="alert alert-primary" role="alert">
    <h4>Liste de vos simulations</h4>
</div>

<!-- таблиця моделювань -->
<table class="table table-sm table-hover table-striped">
    
    <!-- тіло таблиці (відображені дані) -->
    <tbody>
    <!-- кожна симуляція відображається під час перегляду таблиці симуляцій -->
    {% for simulation in modèle.simulations %}

    <!-- відображення рядка таблиці з 6 стовпцями — тег <tr> -->
    <!-- стовпець 1: заголовок рядка (№ симуляції) — тег <th scope='row' -->
    <!-- стовпець 2: значення параметра [marié] — тег <td> -->
    <!-- стовпець 3: значення параметра [enfants] — тег <td> -->
    <!-- стовпець 4: значення параметра [salaire] — тег <td> -->
    <!-- стовпець 5: значення параметра [impôt] (податку) — тег <td> -->
    <!-- стовпець 6: значення параметра [surcôte] — тег <td> -->
    <!-- стовпець 7: значення параметра [décôte] — тег <td> -->
    <!-- стовпець 8: значення параметра [réduction] — тег <td> -->
    <!-- стовпець 9: значення параметра [taux] (податку) — тег <td> -->
    <!-- стовпець 10: посилання на скасування симуляції — тег <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 %}

Фрагмент меню [v-menu.html]


<!-- меню Bootstrap -->
<nav class="nav flex-column">
    <!-- відображення списку посилань 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. Маршрути

Відтепер існує два типи маршрутів, залежно від того, чи використовують вони жетон, чи ні CSRF:

Image

  • [routes_without_csrftoken] — це маршрути без жетона CSRF. Це маршрути попередньої версії;
  • [routes_with_csrftoken] — це маршрути з маркером CSRF.

У [routes_with_csrftoken] маршрути тепер мають додатковий параметр — токен CSRF:


# фронт-контролер
def front_controller() -> tuple:
    # запит перенаправляється до головного контролера
    main_controller = config['mvc']['controllers']['main-controller']
    return main_controller.execute(request, session, config)

@app.route('/', methods=['GET'])
def index() -> tuple:
    # перенаправлення на /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:
    # виконується контролер, пов'язаний з цією дією
    return front_controller()

# аутентифікація користувача
@app.route('/authentifier-utilisateur/<string:csrf_token>', methods=['POST'])
def authentifier_utilisateur(csrf_token: str) -> tuple:
    # виконується контролер, пов'язаний з цією дією
    return front_controller()

# розрахувати-податок
@app.route('/calculer-impot/<string:csrf_token>', methods=['POST'])
def calculer_impot(csrf_token: str) -> tuple:
    # виконується контролер, пов'язаний з цією дією
    return front_controller()

# пакетний розрахунок податку
@app.route('/calculer-impots/<string:csrf_token>', methods=['POST'])
def calculer_impots(csrf_token: str):
    # виконується контролер, пов'язаний з цією дією
    return front_controller()

# перелік-моделювання
@app.route('/lister-simulations/<string:csrf_token>', methods=['GET'])
def lister_simulations(csrf_token: str) -> tuple:
    # виконується контролер, пов'язаний з цією дією
    return front_controller()

# видалити-симуляцію
@app.route('/supprimer-simulation/<int:numero>/<string:csrf_token>', methods=['GET'])
def supprimer_simulation(numero: int, csrf_token: str) -> tuple:
    # виконується контролер, пов'язаний з цією дією
    return front_controller()

# завершення сеансу
@app.route('/fin-session/<string:csrf_token>', methods=['GET'])
def fin_session(csrf_token: str) -> tuple:
    # виконується контролер, пов'язаний з цією дією
    return front_controller()

# показати-розрахунок-податку
@app.route('/afficher-calcul-impot/<string:csrf_token>', methods=['GET'])
def afficher_calcul_impot(csrf_token: str) -> tuple:
    # виконується контролер, пов'язаний з цією дією
    return front_controller()

# отримати-дані-адміністратора
@app.route('/get-admindata/<string:csrf_token>', methods=['GET'])
def get_admindata(csrf_token: str) -> tuple:
    # виконується контролер, пов'язаний з цією дією
    return front_controller()

Тепер усі маршрути мають у своїх параметрах токен CSRF, навіть маршрут [/init-session]. Це означає, що клієнт не може запустити додаток, безпосередньо ввівши URL [/init-session/html], оскільки там бракуватиме токен CSRF. Тепер він обов’язково повинен пройти через URL та [/] у рядках 7–10.

Вибір маршрутів здійснюється в головному скрипті [main]:



# головний потік більше не потребує логгера
logger.close()

# якщо сталася помилка, процес зупиняється
if erreur:
    sys.exit(2)

# імпортуються маршрути веб-додатку
if config['parameters']['with_csrftoken']:
    import routes_with_csrftoken as routes
else:
    import routes_without_csrftoken as routes

# налаштування маршрутів
routes.config = config

# запуск додатка Flask
routes.execute(__name__)
  • рядки 9–13: вибір маршрутів залежно від того, чи використовує додаток токени CSRF;

34.3.5. Контролер [MainController]

При кожному запиті сервер повинен перевіряти наявність токена CSRF. Ми зробимо це в головному контролері [MainController], через який проходять усі запити:


from flask_wtf.csrf import generate_csrf, validate_csrf

       # обробка запиту
        try:
            # реєстрація в журналі
            logger = Logger(config['parameters']['logsFilename'])

            …

            # отримання елементів шляху
            params = request.path.split('/')

            # дія є першим елементом
            action = params[1]

            …

            if config['parameters']['with_csrftoken']:
                # csrf_token є останнім елементом шляху
                csrf_token = params.pop()
                # перевіряється дійсність токена
                # якщо csrf_token не відповідає очікуваному, буде згенеровано виняток
                validate_csrf(csrf_token)

            …

        except ValidationError as exception:
            # недійсний токен CSRF
            résultat = {"action": action, "état"121"réponse"[f"{exception}"]}
            status_code = status.HTTP_400_BAD_REQUEST

        except BaseException as exception:
            # інші (несподівані) винятки
            résultat = {"action": action, "état"131"réponse"[f"{exception}"]}
            status_code = status.HTTP_400_BAD_REQUEST

        finally:
            pass

        # до результату додається csrf_token
        résultat['csrf_token'] = generate_csrf()

        # результат, надісланий клієнту, записується в журнал
        log = f"[MainController] {résultat}\n"
        logger.write(log)
  • рядок 20: отримуємо токен CSRF із URL запиту типу [http://machine :port/chemin/action/param1/param2/…/csrf_token]. Токен сеансу завжди є останнім елементом URL;
  • рядок 23: перевіряється дійсність токена CSRF, отриманого з URL, з токеном CSRF сеансу. Якщо він недійсний, функція [validate_csrf] генерує виняток типу [ValidationError] (рядок 27);
  • рядок 41: токен CSRF додається до результату, що надсилається клієнту. Клієнтам jSON та XML він знадобиться. Адже ці клієнти не отримують сторінок HTML із токеном CSRF у посиланнях, що містяться на цих сторінках. Тому вони отримають його у результаті jSON або XML, надісланому сервером;

Примітка: функція [validate_csrf] у рядку 23 не перевіряє точної збіжності. Токен CSRF зберігається в сесії з ключем [csrf_token]. Тести, здається, показують, що токен CSRF є дійсним, якщо він був згенерований під час сеансу. Отже, якщо вручну, наприклад, у URL, що відображається в браузері (/lister-simulations/xyz), замінити токен [xyz] або CSRF на інший — [abc], який вже було отримано під час попередньої дії, — дія [/lister-simulations] буде виконана успішно;

34.4. Тестування за допомогою браузера

Увімкніть:

  • запускаємо сервер із параметром від [with_csrftoken] до [True];
  • запитуйте URL [http://localhost:5000] за допомогою браузера;

Image

  • у [1] — токен CSRF;

Проведемо операції, доки не отримаємо список симуляцій:

Image

Тепер вручну введемо URL [http://localhost:5000/supprimer-simulation/1/x], щоб видалити симуляцію з id=1. Ми навмисно вводимо неправильний токен CSRF, щоб подивитися, що відбудеться. Відповідь сервера така:

Image

Примітка 1: не можна з упевненістю стверджувати, що метод, який тут використовується, завжди є достатнім для протидії атакам CSRF. Повернемося до схеми атаки:

Image

Якщо скрипт JavaScript, завантажений у [5], здатний зчитувати історію браузера, яким користується Аліса, він зможе отримати URL, виконані браузером, а також URL, такі як [/cible/csrf_token]. Тоді він зможе отримати сесійний токен [csrf_token] і здійснити атаку за допомогою [6-7]. Проте браузер дозволяє використовувати лише історію того вікна браузера, в якому виконується скрипт. Отже, якщо Аліса не використовує те саме вікно для роботи з сайтом A ([1-2]) та читання повідомлення від Малорі ([3]), атака CSRF буде неможливою.

34.5. Консольні клієнти

Інший спосіб перевірити версію 14 додатка — це взяти тести з версії 12 та адаптувати їх до нового сервера.

Image

Папка [impots/http-clients/09] спочатку створюється шляхом копіювання папки [impots/http-clients/07]. Потім її модифікують.

Повернемося до маршрутів, які ініціюють сеанс:


# кореневий каталог додатка
@app.route('/', methods=['GET'])
def index() -> tuple:
    # перенаправлення на /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:
    # виконується контролер, пов'язаний з цією дією
    return front_controller()

Жоден із цих маршрутів не підходить для ініціалізації сесії jSON або XML:

  • рядки 2–5: маршрут [/] ініціює сесію HTML;
  • рядки 8–11: маршрут [/init-session] вимагає токена CSRF, якого ми не маємо;

Ми вирішили додати новий маршрут на сервер:


# init-session-without-csrftoken
@app.route('/init-session-without-csrftoken/<string:type_response>', methods=['GET'])
def init_session_without_csrftoken(type_response: str) -> tuple:
    # перенаправлення на /init-session/type_response
    return redirect(url_for("init_session", type_response=type_response, csrf_token=generate_csrf()), status.HTTP_302_FOUND)
  • рядок 2: новий маршрут. Він не вимагає токена CSRF. Таким чином, ми повернулися до маршруту [/init-session] з попередньої версії;
  • рядки 4–5: ми перенаправляємо клієнта (jSON, XML, HTML) на маршрут [/init-session], який має у своїх параметрах токен CSRF;

Цей новий маршрут можна випробувати за допомогою браузера:

Image

Відповідь сервера (налаштованого з [with_csrftoken=True]) така:

Image

  • у [1] сервер було перенаправлено на маршрут [/init-session] із токеном CSRF у URL;
  • у [2] токен CSRF міститься у словнику jSON, надісланому сервером, пов’язаним із ключем [csrf_token];

Повернемося до коду клієнта:

Image

Ми змінюємо конфігурацію [config] наступним чином:


   config.update({
        # файл платників податків
        "taxpayersFilename"f"{script_dir}/../data/input/taxpayersdata.txt",
        # файл результатів
        "resultsFilename"f"{script_dir}/../data/output/résultats.json",
        # файл помилок
        "errorsFilename"f"{script_dir}/../data/output/errors.txt",
        # файл журналів
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # сервер розрахунку податку
        "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"True,
        # csrf_token
        "with_csrftoken"True,
    }
    )

    # маршрут 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'
  • рядок 31: логічне значення вкаже клієнту, чи працює сервер, до якого він звертається, з токенами CSRF;
  • рядки 37–40: встановлюємо службовий URL для дії [init-session]:
    • якщо сервер використовує токени CSRF, то службовим токеном URL є [/init-session-without-csrftoken];
    • інакше сервісний URL дорівнює [/init-session];

Було представлено маршрут [/init-session-without-csrftoken]. Він дозволяє клієнту jSON / XML розпочати сеанс із сервером, не маючи токена CSRF. Цей токен він знайде у відповіді сервера.

Далі ми модифікуємо клас [ImpôtsDaoWithHttpSession], що реалізує клієнтський рівень [dao]:

Image


# імпорт
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):

    # конструктор
    def __init__(self, config: dict):
        # ініціалізація батьківського об’єкта
        AbstractImpôtsDao.__init__(self, config)
        # збереження елементів конфігурації
        # загальна конфігурація
        self.__config = config
        # сервер
        self.__config_server = config["server"]
        # служби
        self.__config_services = config["server"]['url_services']
        # режим налагодження
        self.__debug = config["debug"]
        # реєстратор
        self.__logger = None
        # файли cookie
        self.__cookies = None
        # тип сеансу (json, xml)
        self.__session_type = None
        # токен CSRF
        self.__csrf_token = None

    # етап запиту/відповіді
    def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
        # [method]: метод HTTP, GET або POST
        # [url_service]: сервісний URL
        # [data]: параметри POST у форматі x-www-form-urlencoded
        # [json]: параметри POST у форматі json
        # [cookies]: файли cookie, які слід включити в запит

        # потрібно мати сесію XML або JSON, інакше не вдасться обробити відповідь
        if self.__session_type not in ['json''xml']:
            raise ImpôtsError(73"il n'y a pas de session valide en cours")

        # до сервісного URL додається токен CSRF
        if self.__csrf_token:
            url_service = f"{url_service}/{self.__csrf_token}"

        # виконання запиту
        response = requests.request(method,
                                    url_service,
                                    data=data_value,
                                    json=json_value,
                                    cookies=self.__cookies,
                                    allow_redirects=True)

        # режим налагодження?
        if self.__debug:
            # реєстратор
            if not self.__logger:
                self.__logger = self.__config['logger']
            # запис у журнал
            self.__logger.write(f"{response.text}\n")

        # результат
        if self.__session_type == "json":
            résultat = json.loads(response.text)
        else:  # xml
            résultat = xmltodict.parse(response.text[39:])['root']

        # отримуємо файли cookie з відповіді, якщо вони є
        if response.cookies:
            self.__cookies = response.cookies

        # отримуємо токен CSRF
        if self.__config['with_csrftoken']:
            self.__csrf_token = résultat.get('csrf_token'None)

        # код статусу
        status_code = response.status_code

        # якщо код статусу відмінний від 200, OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(35, résultat['réponse'])

        # повертаємо результат
        return résultat['réponse']

    
    def init_session(self, session_type: str):
        # фіксується тип сеансу
        self.__session_type = session_type

        # видаляємо токен CSRF з попередніх викликів
        self.__csrf_token = None

        # запитується URL дії init-session
        url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        # виконання запиту
        self.get_response("GET", url_service)

  • рядки 38–92: обробка токена CSRF відбувається переважно в методі [get_response];
  • рядок 60: важливим моментом є параметр [allow_redirects=True]. Це його значення за замовчуванням, але ми вирішили виділити його;

У режимі [with_csrftoken=True]:

  • клієнти розпочинають діалог із сервером, звертаючись до маршруту [/init-session_without_csftoken/type_response];
  • сервер відповідає на цей запит перенаправленням на маршрут [/init-session/type_response/csrf_token];
  • через параметр [allow_redirects=True] це перенаправлення буде виконано клієнтом [requests];
  • токен CSRF буде знайдено в отриманому результаті у рядках 72 та 74, пов’язаному з ключем [csrf_token];

У режимі [with_csrftoken=False]:

  • (продовження)
    • клієнти розпочинають діалог із сервером, звертаючись до маршруту [/init-session /type_response];
    • сервер відповідає на цей запит перенаправленням на маршрут [/init-session/type_response];
    • через параметр [allow_redirects=True] за цим перенаправленням слідуватиме клієнт [requests];
    • у рядках 81–82 немає токена CSRF, який можна було б отримати. Тому властивість [self.__csrf_token] залишається на рівні None (рядок 36);
  • рядки 51–52: для всіх наступних запитів токен CSRF, якщо він існує, додається до початкового маршруту;
  • рядки 81–82: новий токен, який сервер генерує при кожному новому запиті клієнта, зберігається локально, щоб бути відправленим у рядку 52 у наступному запиті;

Крім того, метод [init_session] дещо змінюється:


    def init_session(self, session_type: str):
        # фіксується тип сеансу
        self.__session_type = session_type

        # видаляється токен CSRF з попередніх викликів
        self.__csrf_token = None

        # запитується URL дії init-session
        url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        # виконання запиту
        self.get_response("GET", url_service)

Тут слід пам’ятати, що ми створили маршрут [/init-session-without-csrftoken/<type-response>] для ініціалізації діалогу клієнт/сервер без токена CSRF. Однак ми бачили, що метод [get_response], який викликається у 12-му рядку коду, систематично додає в кінець сервісного URL токен CSRF, збережений у [self.__csrf_token]. Саме тому в рядку 6 коду цей токен CSRF видаляється, якщо він існує.

Ось і все. Для тестування виконаємо:

  • консольні клієнти [main, main2, main3];
  • тестові класи [Test1HttpClientDaoWithSession] та [Test2HttpClientDaoWithSession];

послідовно встановлюючи для параметра конфігурації [with_csrftoken] значення спочатку True, а потім False.

Image

Ось, наприклад, журнали, отримані під час виконання клієнта [main json] разом із [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

Якщо розглянути токени CSRF, отримані послідовно, то можна побачити, що всі вони різні.