Skip to content

34. Exercício prático: versão 14

Image

A pasta [http-servers/09] da versão 14 é obtida através da cópia da pasta [http-servers/08] da versão 13.

34.1. Introdução

CSRF (Cross Site Request Forgery) é uma técnica de roubo de sessão. É explicada da seguinte forma na Wikipédia (https://fr.wikipedia.org/wiki/Cross-site_request_forgery):

Suponhamos que a Alice seja a administradora de um fórum e que esteja ligada ao mesmo através de um sistema de sessões. A Malorie é membro desse mesmo fórum e pretende eliminar uma das mensagens do fórum. Como não possui os direitos necessários com a sua conta, recorre à conta da Alice através de um ataque do tipo CSRF.
  1. Malorie consegue descobrir o link que permite eliminar a mensagem em questão.
  2. Malorie envia uma mensagem a Alice contendo uma pseudo-imagem para visualizar (que, na verdade, é um script). O URL da imagem é o link para o script que permite eliminar a mensagem pretendida.
  3. A Alice tem de ter uma sessão aberta no seu navegador para o site visado pela Malorie. Esta é uma condição necessária para que o ataque seja bem-sucedido de forma silenciosa, sem exigir um pedido de autenticação que alertasse a Alice. Esta sessão tem de dispor dos direitos necessários para executar a solicitação destrutiva da Malorie. Não é necessário que esteja aberta uma aba do navegador no site alvo, nem sequer que o navegador esteja em execução. Basta que a sessão esteja ativa.
  4. A Alice lê a mensagem da Malorie; o seu navegador utiliza a sessão aberta da Alice e não solicita autenticação interativa. Tenta recuperar o conteúdo da imagem. Ao fazê-lo, o navegador aciona o link e apaga a mensagem, recuperando uma página web de texto como conteúdo para a imagem. Como não reconhece o tipo de imagem associado, não exibe nenhuma imagem e a Alice não sabe que a Malorie acabou de a obrigar a apagar uma mensagem contra a sua vontade.

Mesmo explicada desta forma, a técnica do CSRF é difícil de compreender. Vamos fazer um esquema:

Image

  • em [1-2], a Alice comunica com o fórum (Site A). Este fórum mantém uma sessão para cada utilizador. O navegador da Alice armazena localmente este cookie de sessão e reenvia-o sempre que faz uma nova solicitação ao Site A;
  • em [3], a Malorie envia uma mensagem à Alice. Esta lê-a com o seu navegador. A mensagem lida está no formato HTML e contém um link para uma imagem do site B. Na verdade, esse link é um link para um script JavaScript que é executado assim que chega ao navegador da Alice;
  • esse script JavaScript efetua então uma solicitação ao site A. O navegador de Alice envia automaticamente a solicitação com o cookie de sessão armazenado localmente. É aqui que ocorre o ataque: Malorie conseguiu consultar o site A com os direitos (sessão) de Alice. Depois disso, independentemente do que aconteça, o ataque já ocorreu;

Para contrariar este tipo de ataque, o site A pode proceder da seguinte forma:

  • a cada troca [1-2] com a Alice, o site A envia uma chave, doravante designada por token CSRF, que a Alice deve reenviar na solicitação seguinte. Assim, a Alice deve enviar, em cada solicitação, duas informações:
    • o cookie de sessão;
    • o token CSRF recebido na resposta à sua última solicitação ao site A;

A proteção reside aqui: embora o navegador reenvie automaticamente ao site A o cookie de sessão, não faz o mesmo com o token CSRF. Por este motivo, a troca 6-7 efetuada pelo script de ataque será recusada, uma vez que a solicitação 6 não terá enviado o token CSRF;

O site A pode enviar a Alice o token CSRF de várias formas para uma aplicação HTML:

  • pode, em cada pedido, enviar uma página HTML em que todos os links tenham o token CSRF, por exemplo, [http://siteA/chemin/csrf_token]. Na solicitação seguinte, quando a Alice clicar num desses links, o site A terá apenas de recuperar o token CSRF do URL da solicitação e verificar se está correto. É isso que será feito aqui;
  • pode, no caso das páginas HTML que contenham um formulário, enviar este último com um campo oculto [input type=’hidden’] contendo o token CSRF. Este será então enviado automaticamente com o formulário quando a Alice submeter a página. O site A irá recuperar o token CSRF no corpo (body) da solicitação;
  • outras técnicas são possíveis;

34.2. Configuração

Image

Introduzimos na configuração [parameters] da aplicação dois valores booleanos:

  • [with_redissession]: quando definido como True, a aplicação utiliza uma sessão Redis. Quando definido como False, a aplicação utiliza uma sessão Flask normal;
  • [with_csrftoken]: quando definido como True, os URL da aplicação contêm um token CSRF;

        # duração da pausa do thread em segundos
        "sleep_time"0,
        # servidor Redis
        "with_redissession"True,
        "redis": {
            "host""127.0.0.1",
            "port"6379
        },
        # token CSRF
        "with_csrftoken"False,

34.3. Implementação CSRF

Vamos garantir que, quando:


config['parameters']['with_csrftoken']

for igual a [True], a aplicação envie ao navegador do cliente páginas web cujos links contenham um token CSRF.

34.3.1. O módulo [flask_wtf]

A implementação do token CSRF será feita com o módulo [flask_wtf], que instalamos num terminal PyCharm:


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

34.3.2. Os modelos das vistas

Introduzimos uma nova classe nos modelos:

Image

A classe [AbstractBaseModelForView] é a seguinte:


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 ""
  • linha 9: a classe [AbstractBaseModelForView] implementa a interface [InterfaceModelForView] implementada pelas classes dos modelos;
  • linhas 11-13: o método [get_model_for_view] não está implementado;
  • linhas 15-20: o método [get_csrftoken] gera o token CSRF se a aplicação tiver sido configurada para os utilizar. Consoante o caso, a função devolve um token precedido pelo sinal /; caso contrário, devolve uma cadeia vazia. A função [generate_csrf] tem a particularidade de gerar sempre o mesmo valor para uma determinada solicitação do cliente. O processamento de uma solicitação implica a execução de várias funções. A utilização de [generate_csrf] nessas funções gera sempre o mesmo valor. Na solicitação seguinte, por outro lado, é gerado um novo token CSRF;

Todos os modelos M da vista V incluirão o token CSRF da seguinte forma:


class ModelForAuthentificationView(AbstractBaseModelForView):

    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
        # encapsulamos os dados da página no modelo
        modèle = {}
        

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

        # retorna-se o modelo
        return modèle
  • cada classe de modelo estende a classe base [AbstractBaseModelForView];
  • linha 8: o token CSRF é solicitado à classe pai. Obtém-se ou a cadeia vazia, ou uma cadeia do tipo [/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c];

34.3.3. As visualizações

Pelo que acabámos de ver, todas as vistas V terão no seu modelo M o token CSRF. Poderão, portanto, utilizá-lo nos links que contêm. Vejamos alguns exemplos:

O fragmento de autenticação [v_authentification.html]


<!-- formulário HTML - enviamos os seus valores com a ação [authentifier-utilisateur] -->
<form method="post" action="/authentifier-utilisateur{{modèle.csrf_token}}">

    <!-- título -->
    <div class="alert alert-primary" role="alert">
        <h4>Veuillez vous authentifier</h4>
    </div>


</form>
  • linha 2: de acordo com o que acabámos de ver, o URL do atributo [action] será:

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

ou

[/authentifier-utilisateur]

dependendo de a aplicação ter sido configurada para utilizar ou não tokens CSRF;

O fragmento de cálculo do imposto [v-calcul-impot.html]


<!-- formulário HTML enviado -->
<form method="post" action="/calculer-impot{{modèle.csrf_token}}">
    <!-- mensagem em 12 colunas sobre fundo azul -->
    <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>

O fragmento das simulações [v-liste-simulations.html]


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

<!-- tabela de simulações -->
<table class="table table-sm table-hover table-striped">
    
    <!-- corpo da tabela (dados apresentados) -->
    <tbody>
    <!-- cada simulação é apresentada ao percorrer a tabela de simulações -->
    {% for simulation in modèle.simulations %}

    <!-- exibição de uma linha da tabela com 6 colunas - baliza <tr> -->
    <!-- coluna 1: cabeçalho da linha (n.º da simulação) — baliza <th scope='row' -->
    <!-- coluna 2: valor do parâmetro [marié] - baliza <td> -->
    <!-- coluna 3: valor do parâmetro [enfants] - baliza <td> -->
    <!-- coluna 4: valor do parâmetro [salaire] - tag <td> -->
    <!-- coluna 5: valor do parâmetro [impôt] (do imposto) - baliza <td> -->
    <!-- coluna 6: valor do parâmetro [surcôte] - baliza <td> -->
    <!-- coluna 7: valor do parâmetro [décôte] - baliza <td> -->
    <!-- coluna 8: valor do parâmetro [réduction] - tag <td> -->
    <!-- coluna 9: valor do parâmetro [taux] (do imposto) - tag <td> -->
    <!-- coluna 10: link para eliminar a simulação - 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 %}

O fragmento do menu [v-menu.html]


<!-- menu Bootstrap -->
<nav class="nav flex-column">
    <!-- exibição de uma lista de 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. As estradas

Existem agora dois tipos de estradas, consoante utilizem ou não um token CSRF:

Image

  • [routes_without_csrftoken] são as rotas sem token CSRF. Estas são as rotas da versão anterior;
  • [routes_with_csrftoken] são as rotas com o token CSRF.

Em [routes_with_csrftoken], as rotas têm agora um parâmetro adicional, o token CSRF:


# o controlador frontal
def front_controller() -> tuple:
    # a solicitação é encaminhada para o controlador principal
    main_controller = config['mvc']['controllers']['main-controller']
    return main_controller.execute(request, session, config)

@app.route('/', methods=['GET'])
def index() -> tuple:
    # redirecionamento para /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:
    # é executado o controlador associado à ação
    return front_controller()

# autenticar-utilizador
@app.route('/authentifier-utilisateur/<string:csrf_token>', methods=['POST'])
def authentifier_utilisateur(csrf_token: str) -> tuple:
    # executa-se o controlador associado à ação
    return front_controller()

# calcular-imposto
@app.route('/calculer-impot/<string:csrf_token>', methods=['POST'])
def calculer_impot(csrf_token: str) -> tuple:
    # executa-se o controlador associado à ação
    return front_controller()

# cálculo do imposto em lotes
@app.route('/calculer-impots/<string:csrf_token>', methods=['POST'])
def calculer_impots(csrf_token: str):
    # executa-se o controlador associado à ação
    return front_controller()

# listar simulações
@app.route('/lister-simulations/<string:csrf_token>', methods=['GET'])
def lister_simulations(csrf_token: str) -> tuple:
    # executa-se o controlador associado à ação
    return front_controller()

# eliminar-simulação
@app.route('/supprimer-simulation/<int:numero>/<string:csrf_token>', methods=['GET'])
def supprimer_simulation(numero: int, csrf_token: str) -> tuple:
    # executa-se o controlador associado à ação
    return front_controller()

# fim-sessão
@app.route('/fin-session/<string:csrf_token>', methods=['GET'])
def fin_session(csrf_token: str) -> tuple:
    # executa-se o controlador associado à ação
    return front_controller()

# exibir-cálculo-impostos
@app.route('/afficher-calcul-impot/<string:csrf_token>', methods=['GET'])
def afficher_calcul_impot(csrf_token: str) -> tuple:
    # executa-se o controlador associado à ação
    return front_controller()

# obter-dados-administrativos
@app.route('/get-admindata/<string:csrf_token>', methods=['GET'])
def get_admindata(csrf_token: str) -> tuple:
    # é executado o controlador associado à ação
    return front_controller()

Todas as rotas têm agora o token CSRF nos seus parâmetros, incluindo a rota [/init-session]. Isto significa que o cliente não pode iniciar a aplicação digitando diretamente o URL [/init-session/html], pois faltará o token CSRF. Agora, tem de passar obrigatoriamente pelas linhas 7 a 10: URL e [/].

A escolha das rotas é feita no script principal [main]:



# o thread principal já não necessita do logger
logger.close()

# se tiver ocorrido um erro, o processo é interrompido
if erreur:
    sys.exit(2)

# importação das rotas da aplicação web
if config['parameters']['with_csrftoken']:
    import routes_with_csrftoken as routes
else:
    import routes_without_csrftoken as routes

# configuração das rotas
routes.config = config

# inicialização da aplicação Flask
routes.execute(__name__)
  • linhas 9-13: escolha das rotas consoante a aplicação utilize ou não tokens CSRF;

34.3.5. O controlador [MainController]

Em cada pedido, o servidor deve verificar a presença do token CSRF. Faremos isso no controlador principal [MainController], que recebe todos os pedidos:


from flask_wtf.csrf import generate_csrf, validate_csrf

       # processar o pedido
        try:
            # registo
            logger = Logger(config['parameters']['logsFilename'])

            …

            # recuperação dos elementos do caminho
            params = request.path.split('/')

            # a ação é o primeiro elemento
            action = params[1]

            …

            if config['parameters']['with_csrftoken']:
                # o csrf_token é o último elemento do caminho
                csrf_token = params.pop()
                # verifica-se a validade do token
                # será lançada uma exceção se o csrf_token não for o esperado
                validate_csrf(csrf_token)

            …

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

        except BaseException as exception:
            # outras exceções (inesperadas)
            résultat = {"action": action, "état"131"réponse"[f"{exception}"]}
            status_code = status.HTTP_400_BAD_REQUEST

        finally:
            pass

        # adiciona-se o csrf_token ao resultado
        résultat['csrf_token'] = generate_csrf()

        # regista-se o resultado enviado ao cliente
        log = f"[MainController] {résultat}\n"
        logger.write(log)
  • linha 20: recuperamos o token CSRF do URL da solicitação do tipo [http://machine :port/chemin/action/param1/param2/…/csrf_token]. O token de sessão é sempre o último elemento do URL;
  • linha 23: verifica-se a validade do token CSRF recuperado do URL com o token CSRF da sessão. Se não for válido, a função [validate_csrf] lança uma exceção do tipo [ValidationError] (linha 27);
  • linha 41: o token CSRF é incluído no resultado enviado ao cliente. Os clientes jSON e XML irão precisar dele. Com efeito, estes clientes não recebem páginas HTML com o token CSRF nos links contidos nas páginas. Receberão, portanto, esse token no resultado jSON ou XML enviado pelo servidor;

Nota: a função [validate_csrf] da linha 23 não verifica uma correspondência exata. O token CSRF é armazenado na sessão com a chave [csrf_token]. Os testes parecem indicar que um token CSRF é válido se tiver sido gerado durante a sessão. Assim, se manualmente, no URL apresentado no navegador, por exemplo (/lister-simulations/xyz), substituir o token [xyz] CSRF por outro, [abc], já recebido numa ação anterior, a ação [/lister-simulations] será bem-sucedida;

34.4. Testes com um navegador

Proceda da seguinte forma:

  • inicie o servidor com o parâmetro [with_csrftoken] a [True];
  • solicite o URL [http://localhost:5000] com um navegador;

Image

  • em [1], o token CSRF;

Vamos realizar algumas operações até obtermos uma lista de simulações:

Image

Agora, digitemos manualmente o URL [http://localhost:5000/supprimer-simulation/1/x] para eliminar a simulação com id=1. Introduzimos deliberadamente um token CSRF incorreto para ver o que acontece. A resposta do servidor é a seguinte:

Image

Nota 1: não é certo que o método aqui utilizado seja sempre suficiente para combater os ataques CSRF. Voltemos ao esquema do ataque:

Image

Se o script JavaScript descarregado em [5] for capaz de ler o histórico do navegador utilizado pela Alice, será capaz de recuperar as URL executadas pelo navegador, bem como as URL, tais como a [/cible/csrf_token]. Poderá então recuperar o token de sessão [csrf_token] e lançar o seu ataque em [6-7]. No entanto, o navegador permite apenas a exploração do histórico da janela do navegador na qual o script está a ser executado. Portanto, se a Alice não utilizar a mesma janela para aceder ao site A ([1-2]) e ler a mensagem da Malorie ([3]), o ataque (CSRF) não será possível.

34.5. Clientes de consola

Outra forma de testar a versão 14 da aplicação é retomar os testes da versão 12 e adaptá-los ao novo servidor.

Image

A pasta [impots/http-clients/09] é obtida inicialmente através da cópia da pasta [impots/http-clients/07]. Em seguida, é modificada.

Voltemos às rotas que inicializam uma sessão:


# raiz da aplicação
@app.route('/', methods=['GET'])
def index() -> tuple:
    # redirecionamento para /init-session/html
    return redirect(url_for("init_session", type_response="html", csrf_token=generate_csrf()), status.HTTP_302_FOUND)

# inicialização da sessão com token CSRF
@app.route('/init-session/<string:type_response>/<string:csrf_token>', methods=['GET'])
def init_session(type_response: str, csrf_token: str) -> tuple:
    # executa-se o controlador associado à ação
    return front_controller()

Nenhuma destas rotas é adequada para inicializar uma sessão jSON ou XML:

  • linhas 2-5: a rota [/] inicializa uma sessão HTML;
  • linhas 8-11: a rota [/init-session] requer um token CSRF que desconhecemos;

Decidimos adicionar uma nova rota ao servidor:


# inicialização da sessão sem token CSRF
@app.route('/init-session-without-csrftoken/<string:type_response>', methods=['GET'])
def init_session_without_csrftoken(type_response: str) -> tuple:
    # redirecionamento para /init-session/type_response
    return redirect(url_for("init_session", type_response=type_response, csrf_token=generate_csrf()), status.HTTP_302_FOUND)
  • linha 2: a nova rota. Esta não requer o token CSRF. Assim, voltámos à rota [/init-session] da versão anterior;
  • linhas 4-5: redirecionamos o cliente (jSON, XML, HTML) para a rota [/init-session], que tem o token CSRF nos seus parâmetros;

É possível experimentar esta nova rota com um navegador:

Image

A resposta do servidor (configurado com [with_csrftoken=True]) é a seguinte:

Image

  • em [1], o servidor foi redirecionado para a rota [/init-session] com o token CSRF no URL;
  • em [2], o token CSRF encontra-se no dicionário jSON enviado pelo servidor associado à chave [csrf_token];

Voltemos ao código do cliente:

Image

Alteramos a configuração [config] da seguinte forma:


   config.update({
        # ficheiro dos contribuintes
        "taxpayersFilename"f"{script_dir}/../data/input/taxpayersdata.txt",
        # ficheiro de resultados
        "resultsFilename"f"{script_dir}/../data/output/résultats.json",
        # ficheiro de erros
        "errorsFilename"f"{script_dir}/../data/output/errors.txt",
        # ficheiro de registos
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # servidor de cálculo de impostos
        "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",
            }
        },
        # modo de depuração
        "debug"True,
        # csrf_token
        "with_csrftoken"True,
    }
    )

    # rota 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'
  • linha 31: um valor booleano indicará ao cliente se o servidor a que se dirige trabalha ou não com tokens CSRF;
  • linhas 37-40: definimos o URL de serviço da ação [init-session]:
    • se o servidor utilizar tokens CSRF, então o token de serviço URL é [/init-session-without-csrftoken];
    • caso contrário, o serviço URL é o [/init-session];

Foi apresentada a rota [/init-session-without-csrftoken]. Esta permite que um cliente jSON / XML inicie uma sessão com o servidor sem possuir um token CSRF. Encontrará esse token na resposta do servidor.

Em seguida, alteramos a classe [ImpôtsDaoWithHttpSession] que implementa a camada [dao] do cliente:

Image


# importações
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):

    # construtor
    def __init__(self, config: dict):
        # inicialização do pai
        AbstractImpôtsDao.__init__(self, config)
        # armazenamento dos elementos de configuração
        # configuração geral
        self.__config = config
        # servidor
        self.__config_server = config["server"]
        # serviços
        self.__config_services = config["server"]['url_services']
        # modo de depuração
        self.__debug = config["debug"]
        # registo
        self.__logger = None
        # cookies
        self.__cookies = None
        # tipo de sessão (json, xml)
        self.__session_type = None
        # token CSRF
        self.__csrf_token = None

    # etapa de pedido/resposta
    def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
        # [method]: método HTTP, GET ou POST
        # [url_service]: URL de serviço
        # [data]: parâmetros do POST em x-www-form-urlencoded
        # [json]: parâmetros do POST em JSON
        # [cookies]: cookies a incluir na solicitação

        # é necessário ter uma sessão XML ou JSON; caso contrário, não será possível processar a resposta
        if self.__session_type not in ['json''xml']:
            raise ImpôtsError(73"il n'y a pas de session valide en cours")

        # adiciona-se o token CSRF ao URL de serviço
        if self.__csrf_token:
            url_service = f"{url_service}/{self.__csrf_token}"

        # execução do pedido
        response = requests.request(method,
                                    url_service,
                                    data=data_value,
                                    json=json_value,
                                    cookies=self.__cookies,
                                    allow_redirects=True)

        # modo de depuração?
        if self.__debug:
            # registo
            if not self.__logger:
                self.__logger = self.__config['logger']
            # a registar
            self.__logger.write(f"{response.text}\n")

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

        # recuperam-se os cookies da resposta, caso existam
        if response.cookies:
            self.__cookies = response.cookies

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

        # código de estado
        status_code = response.status_code

        # se o código de estado for diferente de 200, OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(35, résultat['réponse'])

        # retornamos o resultado
        return résultat['réponse']

    
    def init_session(self, session_type: str):
        # regista-se o tipo de sessão
        self.__session_type = session_type

        # elimina-se o token CSRF das chamadas anteriores
        self.__csrf_token = None

        # solicita-se o URL da ação init-session
        url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        # execução da solicitação
        self.get_response("GET", url_service)

  • linhas 38-92: a gestão do token CSRF ocorre principalmente no método [get_response];
  • linha 60: o ponto importante é o parâmetro [allow_redirects=True]. Trata-se do seu valor por predefinição, mas fizemos questão de o destacar;

Quando se está no modo [with_csrftoken=True]:

  • os clientes iniciam o seu diálogo com o servidor através da chamada à rota [/init-session_without_csftoken/type_response];
  • o servidor responde a este pedido com um redirecionamento para a rota [/init-session/type_response/csrf_token];
  • devido ao parâmetro [allow_redirects=True], este redirecionamento será seguido pelo cliente [requests];
  • o token CSRF será encontrado no resultado obtido nas linhas 72 e 74, associado à chave [csrf_token];

Quando se está no modo [with_csrftoken=False]:

  • (continuação)
    • os clientes iniciam o seu diálogo com o servidor através da chamada à rota [/init-session /type_response];
    • o servidor responde a este pedido com um redirecionamento para a rota [/init-session/type_response];
    • devido ao parâmetro [allow_redirects=True], este redirecionamento será seguido pelo cliente [requests];
    • não há nenhum token CSRF para recuperar nas linhas 81-82. A propriedade [self.__csrf_token] permanece, portanto, sempre como None (linha 36);
  • linhas 51-52: para todas as solicitações seguintes, o token CSRF, caso exista, é adicionado à rota inicial;
  • linhas 81-82: o novo token gerado pelo servidor a cada nova solicitação do cliente é armazenado localmente para ser reenviado na linha 52 na solicitação seguinte;

Além disso, o método [init_session] sofre uma ligeira alteração:


    def init_session(self, session_type: str):
        # regista-se o tipo de sessão
        self.__session_type = session_type

        # elimina-se o token CSRF das chamadas anteriores
        self.__csrf_token = None

        # solicita-se o URL da ação init-session
        url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        # execução da consulta
        self.get_response("GET", url_service)

É importante recordar aqui que criámos uma rota [/init-session-without-csrftoken/<type-response>] para inicializar o diálogo cliente/servidor sem o token CSRF. Ora, vimos que o método [get_response], chamado na linha 12 do código, adiciona sistematicamente ao final do serviço URL o token CSRF armazenado em [self.__csrf_token]. É por isso que, na linha 6 do código, se elimina este token CSRF, caso exista.

É tudo. Para os testes, executaremos:

  • os clientes de consola [main, main2, main3];
  • as classes de teste [Test1HttpClientDaoWithSession] e [Test2HttpClientDaoWithSession];

definindo sucessivamente como True e depois como False o parâmetro de configuração [with_csrftoken].

Image

Eis, a título de exemplo, os registos obtidos aquando da execução do cliente [main json] com o [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

Se observarmos os tokens CSRF recebidos sucessivamente, verificamos que são todos diferentes.