34. Toepassingsopdracht: versie 14

De map [http-servers/09] van versie 14 wordt verkregen door de map [http-servers/08] van versie 13 te kopiëren.
34.1. Inleiding
CSRF (Cross Site Request Forgery) is een techniek voor het stelen van sessies. Deze wordt als volgt uitgelegd op Wikipedia (https://fr.wikipedia.org/wiki/Cross-site_request_forgery):
- Malorie weet de link te achterhalen waarmee het betreffende bericht kan worden verwijderd.
- Malorie stuurt Alice een bericht met daarin een zogenaamde afbeelding (die in feite een script is). De URL van de afbeelding is de link naar het script waarmee het gewenste bericht kan worden verwijderd.
- Alice moet een geopende sessie in haar browser hebben voor de website waarop Malorie zich richt. Dit is een vereiste om de aanval onopgemerkt te laten slagen, zonder dat er een authenticatieverzoek nodig is dat Alice zou alarmeren. Deze sessie moet over de benodigde rechten beschikken om het vernietigende verzoek van Malorie uit te voeren. Het is niet nodig dat er een tabblad in de browser is geopend op de doelsite, noch dat de browser zelfs maar is gestart. Het volstaat dat de sessie actief is.
- Alice leest het bericht van Malorie; haar browser maakt gebruik van de geopende sessie van Alice en vraagt niet om interactieve authenticatie. De browser probeert de inhoud van de afbeelding op te halen. Daarbij activeert de browser de link en verwijdert het bericht; hij haalt een tekstpagina op als inhoud voor de afbeelding. Omdat de browser het bijbehorende afbeeldingstype niet herkent, wordt er geen afbeelding weergegeven en weet Alice niet dat Malorie haar zojuist tegen haar wil een bericht heeft laten verwijderen.
Zelfs als het zo wordt uitgelegd, is de techniek van CSRF moeilijk te begrijpen. Laten we een schema maken:

- in [1-2] communiceert Alice met het forum (Site A). Dit forum houdt voor elke gebruiker een sessie bij. De browser van Alice slaat deze sessiecookie lokaal op en stuurt deze telkens terug wanneer hij een nieuw verzoek naar site A doet;
- in [3] stuurt Malorie een bericht naar Alice. Alice leest dit met haar browser. Het gelezen bericht heeft de indeling HTML en bevat een link naar een afbeelding op site B. In feite is deze link een link naar een JavaScript-script dat wordt uitgevoerd zodra het in de browser van Alice terechtkomt;
- dit JavaScript-script voert vervolgens een verzoek uit naar website A. De browser van Alice verstuurt dit verzoek dan automatisch, samen met de lokaal opgeslagen sessiecookie. Hier vindt de aanval plaats: Malorie is erin geslaagd om website A te benaderen met de rechten (sessie) van Alice. Wat er daarna ook gebeurt, de aanval heeft plaatsgevonden;
Om dit soort aanvallen tegen te gaan, kan website A als volgt te werk gaan:
- bij elke uitwisseling [1-2] met Alice stuurt website A een sleutel, hierna ‘token’ (jeton) CSRF genoemd, die Alice bij het volgende verzoek moet terugsturen. Alice moet dus bij elk verzoek twee gegevens versturen:
- de sessiecookie;
- het token CSRF dat ze heeft ontvangen bij het antwoord op haar laatste verzoek aan website A;
Hierin zit de beveiliging: hoewel de browser het sessiecookie automatisch terugstuurt naar website A, doet hij dat niet voor het token CSRF. Om deze reden zal de uitwisseling 6-7 die door het aanvalsscript wordt uitgevoerd, worden geweigerd, omdat verzoek 6 het token CSRF niet heeft meegestuurd;
Site A kan het token CSRF op verschillende manieren naar Alice sturen voor een applicatie met token HTML:
- hij kan bij elk verzoek een pagina HTML versturen waarin alle links het token CSRF bevatten, bijvoorbeeld [http://siteA/chemin/csrf_token]. Bij de volgende verzoek, wanneer Alice op een van deze links klikt, hoeft site A alleen maar het token CSRF uit de URL van het verzoek op te halen en te controleren of het correct is. Dit is wat hier zal gebeuren;
- voor pagina’s die een formulier bevatten, kan de website dit formulier verzenden met een verborgen veld dat het token bevat. Dit token wordt vervolgens automatisch samen met het formulier verzonden wanneer Alice de pagina verstuurt. Site A haalt het token CSRF op uit de body van het verzoek;
- er zijn ook andere technieken mogelijk;
34.2. Configuratie

We voegen in de configuratie [parameters] van de applicatie twee booleaanse waarden toe:
- [with_redissession]: bij True gebruikt de applicatie een Redis-sessie. Bij False gebruikt de applicatie een normale Flask-sessie;
- [with_csrftoken]: bij True bevatten de URL van de applicatie een CSRF-token;
# pauzetijd van de thread in seconden
"sleep_time": 0,
# Redis-server
"with_redissession": True,
"redis": {
"host": "127.0.0.1",
"port": 6379
},
# CSRF-token
"with_csrftoken": False,
34.3. Implementatie CSRF
We zorgen ervoor dat wanneer:
config['parameters']['with_csrftoken']
gelijk is aan [True], de applicatie webpagina’s naar de clientbrowser verstuurt waarvan de links een token CSRF bevatten.
34.3.1. De module [flask_wtf]
De implementatie van het token CSRF gebeurt met de module [flask_wtf], die we installeren in een 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. De sjablonen voor de weergaven
We introduceren een nieuwe klasse in de sjablonen:

De klasse [AbstractBaseModelForView] ziet er als volgt uit:
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 ""
- regel 9: de klasse [AbstractBaseModelForView] implementeert de interface [InterfaceModelForView] die door de modelklassen wordt geïmplementeerd;
- regels 11-13: de methode [get_model_for_view] is niet geïmplementeerd;
- regels 15-20: de methode [get_csrftoken] genereert het token CSRF als de applicatie is geconfigureerd om deze te gebruiken. Afhankelijk van het geval retourneert de functie een token voorafgegaan door het teken /, anders een lege tekenreeks. De functie [generate_csrf] heeft de eigenschap dat deze voor een bepaald clientverzoek altijd dezelfde waarde genereert. De verwerking van een verzoek omvat de uitvoering van verschillende functies. Het gebruik van [generate_csrf] in deze functies genereert altijd dezelfde waarde. Bij het volgende verzoek wordt daarentegen een nieuw token CSRF gegenereerd;
Alle M-sjablonen van weergave V zullen het token CSRF op de volgende manier opnemen:
class ModelForAuthentificationView(AbstractBaseModelForView):
def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
# de gegevens van de pagina worden in een sjabloon ingekapseld
modèle = {}
…
# CSRF-token
modèle['csrf_token'] = super().get_csrftoken(config)
# het sjabloon wordt weergegeven
return modèle
- elke sjabloonklasse is een uitbreiding van de basisklasse [AbstractBaseModelForView];
- regel 8: het token CSRF wordt opgevraagd bij de bovenliggende klasse. Dit levert ofwel de lege tekenreeks op, ofwel een tekenreeks van het type [/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c];
34.3.3. De weergaven
Zoals we zojuist hebben gezien, zullen alle weergaven V in hun model M het token CSRF bevatten. Ze kunnen dit dus gebruiken in de links die ze bevatten. Laten we een paar voorbeelden bekijken:
Het authenticatiefragment [v_authentification.html]
<!-- formulier HTML - de waarden worden verzonden met de actie [authentifier-utilisateur] -->
<form method="post" action="/authentifier-utilisateur{{modèle.csrf_token}}">
<!-- titel -->
<div class="alert alert-primary" role="alert">
<h4>Veuillez vous authentifier</h4>
</div>
…
</form>
- regel 2: op basis van wat we zojuist hebben gezien, zal de URL van het attribuut [action] zijn:
[/authentifier-utilisateur/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c]
of
afhankelijk van of de applicatie is geconfigureerd om al dan niet CSRF-tokens te gebruiken;
Het belastingberekeningsfragment [v-calcul-impot.html]
<!-- formulier HTML verzonden -->
<form method="post" action="/calculer-impot{{modèle.csrf_token}}">
<!-- bericht in 12 kolommen op een blauwe achtergrond -->
<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>
Het simulatiefragment [v-liste-simulations.html]
{% if modèle.simulations is undefined or modèle.simulations|length==0 %}
<!-- bericht op blauwe achtergrond -->
<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 %}
<!-- bericht op blauwe achtergrond -->
<div class="alert alert-primary" role="alert">
<h4>Liste de vos simulations</h4>
</div>
<!-- simulatietabel -->
<table class="table table-sm table-hover table-striped">
…
<!-- hoofdgedeelte van de tabel (weergegeven gegevens) -->
<tbody>
<!-- elke simulatie wordt weergegeven door de simulatietabel te doorlopen -->
{% for simulation in modèle.simulations %}
<!-- weergave van een rij van de tabel met 6 kolommen - tag <tr> -->
<!-- kolom 1: rijkop (simulatienummer) – tag <th scope='row' -->
<!-- kolom 2: parameterwaarde [marié] - tag <td> -->
<!-- kolom 3: parameterwaarde [enfants] - tag <td> -->
<!-- kolom 4: parameterwaarde [salaire] - tag <td> -->
<!-- kolom 5: parameterwaarde [impôt] (van de belasting) - tag <td> -->
<!-- kolom 6: parameterwaarde [surcôte] - tag <td> -->
<!-- kolom 7: parameterwaarde [décôte] - tag <td> -->
<!-- kolom 8: parameterwaarde [réduction] - tag <td> -->
<!-- kolom 9: parameterwaarde [taux] (van de belasting) - tag <td> -->
<!-- kolom 10: link om de simulatie te verwijderen - 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 %}
Het menu-fragment [v-menu.html]
<!-- Bootstrap-menu -->
<nav class="nav flex-column">
<!-- weergave van een lijst met 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. De routes
Er zijn nu twee soorten routes, afhankelijk van of ze al dan niet een token gebruiken: CSRF:

- [routes_without_csrftoken] zijn de routes zonder token CSRF. Dit zijn de routes uit de vorige versie;
- [routes_with_csrftoken] zijn de routes met het token CSRF.
In [routes_with_csrftoken] hebben de routes nu een extra parameter, het token CSRF:
# de frontcontroller
def front_controller() -> tuple:
# het verzoek wordt doorgestuurd naar de hoofdcontroller
main_controller = config['mvc']['controllers']['main-controller']
return main_controller.execute(request, session, config)
@app.route('/', methods=['GET'])
def index() -> tuple:
# omleiding naar /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:
# de bij de actie behorende controller wordt uitgevoerd
return front_controller()
# gebruikersauthenticatie
@app.route('/authentifier-utilisateur/<string:csrf_token>', methods=['POST'])
def authentifier_utilisateur(csrf_token: str) -> tuple:
# de bij de actie behorende controller wordt uitgevoerd
return front_controller()
# belasting-berekenen
@app.route('/calculer-impot/<string:csrf_token>', methods=['POST'])
def calculer_impot(csrf_token: str) -> tuple:
# de bij de actie behorende controller wordt uitgevoerd
return front_controller()
# belasting berekenen in batches
@app.route('/calculer-impots/<string:csrf_token>', methods=['POST'])
def calculer_impots(csrf_token: str):
# de bij de actie behorende controller wordt uitgevoerd
return front_controller()
# simulaties weergeven
@app.route('/lister-simulations/<string:csrf_token>', methods=['GET'])
def lister_simulations(csrf_token: str) -> tuple:
# de bij de actie behorende controller wordt uitgevoerd
return front_controller()
# simulatie verwijderen
@app.route('/supprimer-simulation/<int:numero>/<string:csrf_token>', methods=['GET'])
def supprimer_simulation(numero: int, csrf_token: str) -> tuple:
# de bij de actie behorende controller wordt uitgevoerd
return front_controller()
# einde-sessie
@app.route('/fin-session/<string:csrf_token>', methods=['GET'])
def fin_session(csrf_token: str) -> tuple:
# de bij de actie behorende controller wordt uitgevoerd
return front_controller()
# belastingberekening weergeven
@app.route('/afficher-calcul-impot/<string:csrf_token>', methods=['GET'])
def afficher_calcul_impot(csrf_token: str) -> tuple:
# de bij de actie behorende controller wordt uitgevoerd
return front_controller()
# get-admindata
@app.route('/get-admindata/<string:csrf_token>', methods=['GET'])
def get_admindata(csrf_token: str) -> tuple:
# de bij de actie behorende controller wordt uitgevoerd
return front_controller()
Alle routes hebben nu het token CSRF in hun parameters, zelfs de route [/init-session]. Dit betekent dat de klant de applicatie niet kan starten door rechtstreeks URL [/init-session/html] in te voeren, omdat dan het token CSRF ontbreekt. Hij moet nu verplicht via de URL en [/] uit de regels 7-10 gaan.
De routekeuze wordt gemaakt in het hoofdscript [main]:
…
# de hoofdthread heeft de logger niet meer nodig
logger.close()
# als er een fout is opgetreden, wordt het proces gestopt
if erreur:
sys.exit(2)
# de routes van de webapplicatie worden geïmporteerd
if config['parameters']['with_csrftoken']:
import routes_with_csrftoken as routes
else:
import routes_without_csrftoken as routes
# configuratie van de routes
routes.config = config
# de Flask-applicatie starten
routes.execute(__name__)
- regels 9-13: keuze van de routes, afhankelijk van het feit of de applicatie al dan niet CSRF-tokens gebruikt;
34.3.5. De controller [MainController]
Bij elk verzoek moet de server controleren of het token CSRF aanwezig is. Dit doen we in de hoofdcontroller [MainController], die alle verzoeken verwerkt:
from flask_wtf.csrf import generate_csrf, validate_csrf
…
# het verzoek wordt verwerkt
try:
# loggen
logger = Logger(config['parameters']['logsFilename'])
…
# de elementen van het pad worden opgehaald
params = request.path.split('/')
# de actie is het eerste element
action = params[1]
…
if config['parameters']['with_csrftoken']:
# de csrf_token is het laatste element van het pad
csrf_token = params.pop()
# de geldigheid van het token wordt gecontroleerd
# er wordt een uitzondering gegenereerd als csrf_token niet het verwachte token is
validate_csrf(csrf_token)
…
except ValidationError as exception:
# ongeldig CSRF-token
résultat = {"action": action, "état": 121, "réponse": [f"{exception}"]}
status_code = status.HTTP_400_BAD_REQUEST
except BaseException as exception:
# andere (onverwachte) uitzonderingen
résultat = {"action": action, "état": 131, "réponse": [f"{exception}"]}
status_code = status.HTTP_400_BAD_REQUEST
finally:
pass
# csrf_token wordt aan het resultaat toegevoegd
résultat['csrf_token'] = generate_csrf()
# het naar de klant verzonden resultaat wordt gelogd
log = f"[MainController] {résultat}\n"
logger.write(log)
- regel 20: we halen het token CSRF op uit de URL van het verzoek van het type [http://machine :port/chemin/action/param1/param2/…/csrf_token]. Het sessietoken is altijd het laatste element van de URL;
- regel 23: de geldigheid van het token CSRF, opgehaald uit het URL, wordt gecontroleerd aan de hand van het token CSRF van de sessie. Als het niet geldig is, genereert de functie [validate_csrf] een uitzondering van het type [ValidationError] (regel 27);
- regel 41: het token CSRF wordt opgenomen in het resultaat dat naar de klant wordt verzonden. De clients jSON en XML hebben dit nodig. Deze clients ontvangen namelijk geen HTML-pagina’s met het token CSRF in de links op die pagina’s. Ze zullen het dus ontvangen in het resultaat jSON of XML dat door de server wordt verzonden;
Opmerking: de functie [validate_csrf] op regel 23 controleert niet op een exacte overeenkomst. Het token CSRF wordt in de sessie opgeslagen met de sleutel [csrf_token]. Uit tests blijkt dat een token CSRF geldig is als het tijdens de sessie is gegenereerd. Als u dus handmatig, in de URL die in de browser wordt weergegeven, bijvoorbeeld (/lister-simulations/xyz), het token [xyz] of CSRF vervangt door een ander token, bijvoorbeeld [abc], dat u al bij een eerdere actie hebt ontvangen, dan zal de actie [/lister-simulations] slagen;
34.4. Tests met een browser
We:
- start de server met de parameter [with_csrftoken] tot [True];
- vraag de URL [http://localhost:5000] op met een browser;

- in [1], het token CSRF;
Laten we wat bewerkingen uitvoeren totdat we een lijst met simulaties hebben:

Laten we nu handmatig URL [http://localhost:5000/supprimer-simulation/1/x] invoeren om de simulatie met id=1 te verwijderen. We voeren opzettelijk een onjuist token CSRF in om te zien wat er gebeurt. Het antwoord van de server is als volgt:

Opmerking 1: het is niet zeker of de hier gebruikte methode altijd voldoende is om CSRF-aanvallen tegen te gaan. Laten we terugkeren naar het schema van de aanval:

Als het JavaScript-script dat is gedownload in [5] de browsergeschiedenis van Alice kan lezen, dan kan het de door de browser uitgevoerde URL-scripts achterhalen, zoals URL en [/cible/csrf_token]. Vervolgens kan het de sessietoken [csrf_token] ophalen en zijn aanval uitvoeren in [6-7]. De browser staat echter alleen het gebruik toe van de geschiedenis van het browservenster waarin het script wordt uitgevoerd. Als Alice dus niet hetzelfde venster gebruikt om met website A ([1-2]) te werken en het bericht van Malorie ([3]) te lezen, is de aanval (CSRF) niet mogelijk.
34.5. Console-clients
Een andere manier om versie 14 van de applicatie te testen, is door de tests van versie 12 over te nemen en deze aan te passen aan de nieuwe server.

De map [impots/http-clients/09] wordt in eerste instantie verkregen door de map [impots/http-clients/07] te kopiëren. Vervolgens wordt deze gewijzigd.
Laten we teruggaan naar de routes die een sessie initialiseren:
# hoofdmap van de applicatie
@app.route('/', methods=['GET'])
def index() -> tuple:
# omleiding naar /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:
# de bij de actie behorende controller wordt uitgevoerd
return front_controller()
Geen van deze routes is geschikt om een sessie jSON of XML te initialiseren:
- regels 2-5: de route [/] initialiseert een sessie HTML;
- regels 8-11: de route [/init-session] vereist een token CSRF dat we niet kennen;
We besluiten een nieuwe route aan de server toe te voegen:
# init-session-without-csrftoken
@app.route('/init-session-without-csrftoken/<string:type_response>', methods=['GET'])
def init_session_without_csrftoken(type_response: str) -> tuple:
# omleiding naar /init-session/type_response
return redirect(url_for("init_session", type_response=type_response, csrf_token=generate_csrf()), status.HTTP_302_FOUND)
- regel 2: de nieuwe route. Deze vereist geen token CSRF. We zijn dus terug bij de route [/init-session] uit de vorige versie;
- regels 4-5: we leiden de client om (jSON, XML, HTML) om naar de route [/init-session], die het token CSRF in zijn parameters heeft;
Je kunt deze nieuwe route met een browser uitproberen:

Het antwoord van de server (geconfigureerd met [with_csrftoken=True]) is als volgt:

- bij [1] is de server omgeleid naar de route [/init-session] met token CSRF in URL;
- in [2] staat het token CSRF in het woordenboek jSON dat door de server is verzonden en gekoppeld is aan de sleutel [csrf_token];
Laten we teruggaan naar de clientcode:

We wijzigen de configuratie [config] als volgt:
config.update({
# belastingplichtigenbestand
"taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
# resultatenbestand
"resultsFilename": f"{script_dir}/../data/output/résultats.json",
# foutbestand
"errorsFilename": f"{script_dir}/../data/output/errors.txt",
# logbestand
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# de server voor belastingberekening
"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",
}
},
# debugmodus
"debug": True,
# csrf_token
"with_csrftoken": True,
}
)
…
# route init-session
url_services = config['server']['url_services']
if config['with_csrftoken']:
url_services['init-session'] = '/init-session-without-csrftoken'
else:
url_services['init-session'] = '/init-session'
- regel 31: een booleaanse waarde geeft aan de client aan of de server waarmee hij communiceert al dan niet met CSRF-tokens werkt;
- regels 37-40: we stellen de URL-servicecode van de actie [init-session] in:
- als de server CSRF-tokens gebruikt, dan is de URL-servicetoken [/init-session-without-csrftoken];
- anders is de URL-service [/init-session];
De route [/init-session-without-csrftoken] is geïntroduceerd. Hiermee kan een client met jSON / XML een sessie met de server starten zonder in het bezit te zijn van een token CSRF. Hij vindt dit token in het antwoord van de server.
Vervolgens passen we de klasse [ImpôtsDaoWithHttpSession] aan, die de [dao]-laag van de client implementeert:

# imports
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):
# constructor
def __init__(self, config: dict):
# initialisatie van bovenliggend element
AbstractImpôtsDao.__init__(self, config)
# opslag van configuratie-elementen
# algemene configuratie
self.__config = config
# server
self.__config_server = config["server"]
# services
self.__config_services = config["server"]['url_services']
# debugmodus
self.__debug = config["debug"]
# logger
self.__logger = None
# cookies
self.__cookies = None
# sessietype (json, xml)
self.__session_type = None
# token CSRF
self.__csrf_token = None
# verzoek-/antwoordfase
def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
# [method]: methode HTTP GET of POST
# [url_service]: URL-service
# [data]: instellingen van POST in x-www-form-urlencoded
# [json]: parameters van POST in JSON
# [cookies]: cookies die in het verzoek moeten worden opgenomen
# er moet een sessie zijn met XML of JSON, anders kan het antwoord niet worden verwerkt
if self.__session_type not in ['json', 'xml']:
raise ImpôtsError(73, "il n'y a pas de session valide en cours")
# het token CSRF wordt toegevoegd aan de servicetoken URL
if self.__csrf_token:
url_service = f"{url_service}/{self.__csrf_token}"
# het verzoek wordt uitgevoerd
response = requests.request(method,
url_service,
data=data_value,
json=json_value,
cookies=self.__cookies,
allow_redirects=True)
# debugmodus?
if self.__debug:
# logboek
if not self.__logger:
self.__logger = self.__config['logger']
# wordt gelogd
self.__logger.write(f"{response.text}\n")
# resultaat
if self.__session_type == "json":
résultat = json.loads(response.text)
else: # xml
résultat = xmltodict.parse(response.text[39:])['root']
# we halen de cookies uit het antwoord op, indien aanwezig
if response.cookies:
self.__cookies = response.cookies
# het token wordt opgehaald CSRF
if self.__config['with_csrftoken']:
self.__csrf_token = résultat.get('csrf_token', None)
# statuscode
status_code = response.status_code
# als de statuscode anders is dan 200 OK
if status_code != status.HTTP_200_OK:
raise ImpôtsError(35, résultat['réponse'])
# we geven het resultaat weer
return résultat['réponse']
def init_session(self, session_type: str):
# het type van de sessie wordt genoteerd
self.__session_type = session_type
# het token CSRF van eerdere oproepen wordt verwijderd
self.__csrf_token = None
# we vragen de URL van de actie init-session
url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
# verzoek wordt uitgevoerd
self.get_response("GET", url_service)
…
- regels 38-92: het beheer van het token CSRF vindt voornamelijk plaats in de methode [get_response];
- regel 60: het belangrijkste punt is de parameter [allow_redirects=True]. Dit is de standaardwaarde, maar we hebben er toch de nadruk op willen leggen;
Wanneer men zich in de modus [with_csrftoken=True] bevindt:
- beginnen de clients hun dialoog met de server door de route [/init-session_without_csftoken/type_response] aan te roepen;
- reageert de server op dit verzoek met een omleiding naar de route [/init-session/type_response/csrf_token];
- vanwege de parameter [allow_redirects=True] zal deze omleiding worden gevolgd door de client [requests];
- het token CSRF is te vinden in het opgehaalde resultaat op de regels 72 en 74, gekoppeld aan de sleutel [csrf_token];
Wanneer men zich in de modus [with_csrftoken=False] bevindt:
- (vervolg)
- beginnen de clients hun dialoog met de server door de route [/init-session /type_response] aan te roepen;
- de server beantwoordt dit verzoek met een omleiding naar de route [/init-session/type_response];
- vanwege de parameter [allow_redirects=True] wordt deze omleiding door de client gevolgd door [requests];
- er is geen token CSRF om op te halen in de regels 81-82. De eigenschap [self.__csrf_token] blijft dan altijd op None staan (regel 36);
- regels 51-52: voor alle volgende verzoeken wordt het token CSRF, indien aanwezig, toegevoegd aan de oorspronkelijke route;
- regels 81-82: het nieuwe token dat de server bij elk nieuw verzoek van de client genereert, wordt lokaal opgeslagen om in regel 52 bij het volgende verzoek te worden teruggestuurd;
Bovendien is de methode [init_session] enigszins gewijzigd:
def init_session(self, session_type: str):
# het type van de sessie wordt genoteerd
self.__session_type = session_type
# het token CSRF van de vorige oproepen wordt verwijderd
self.__csrf_token = None
# de URL van de actie init-session wordt opgevraagd
url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
# verzoek wordt uitgevoerd
self.get_response("GET", url_service)
We moeten hier in gedachten houden dat we een route [/init-session-without-csrftoken/<type-response>] hebben aangemaakt om de client/server-dialoog te initialiseren zonder het token CSRF. We hebben echter gezien dat de methode [get_response], die in regel 12 van de code wordt aangeroepen, systematisch aan het einde van de serviceroute URL het token CSRF toevoegt dat is opgeslagen in [self.__csrf_token]. Daarom wordt in regel 6 van de code dit token CSRF verwijderd, mocht het bestaan.
Dat is alles. Voor het testen voeren we het volgende uit:
- de console-clients [main, main2, main3];
- de testklassen [Test1HttpClientDaoWithSession] en [Test2HttpClientDaoWithSession];
waarbij we de configuratieparameter [with_csrftoken] achtereenvolgens op True en vervolgens op False zetten.

Hieronder ziet u als voorbeeld de logbestanden die zijn verkregen bij het uitvoeren van de client [main json] met [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
Als we kijken naar de CSRF-tokens die achtereenvolgens zijn ontvangen, zien we dat ze allemaal verschillend zijn.