Skip to content

34. تمرين عملي: الإصدار 14

يتم الحصول على المجلد [http-servers/09] للإصدار 14 عن طريق نسخ المجلد [http-servers/08] من الإصدار 13.

34.1. مقدمة

CSRF (تزوير الطلبات عبر المواقع) هي تقنية لاختطاف الجلسة. يتم شرحها على النحو التالي في ويكيبيديا (https://fr.wikipedia.org/wiki/Cross-site_request_forgery):

لنفترض أن أليس هي مديرة منتدى وتسجل الدخول إليه عبر نظام جلسات. مالوري هي عضوة في نفس المنتدى وتريد حذف إحدى مشاركات المنتدى. وبما أنها لا تملك الأذونات اللازمة في حسابها، فإنها تستخدم حساب أليس عبر هجوم CSRF.
  1. تمكنت مالوري من العثور على الرابط الذي يسمح لها بحذف الرسالة المعنية.
  2. ترسل مالوري رسالة إلى أليس تحتوي على صورة زائفة لعرضها (وهي في الواقع برنامج نصي). عنوان URL للصورة هو الرابط إلى البرنامج النصي الذي يحذف الرسالة المطلوبة.
  3. يجب أن يكون لدى أليس جلسة عمل مفتوحة في متصفحها للموقع الذي تستهدفه مالوري. هذا شرط أساسي لنجاح الهجوم بصمت دون إثارة طلب مصادقة من شأنه تنبيه أليس. يجب أن تتمتع هذه الجلسة بالأذونات اللازمة لتنفيذ طلب مالوري التدميري. ليس من الضروري أن تكون علامة تبويب المتصفح مفتوحة على الموقع المستهدف، ولا حتى أن يكون المتصفح قيد التشغيل. يكفي أن تكون الجلسة نشطة.
  4. تقرأ أليس رسالة مالوري؛ ويستخدم متصفحها الجلسة المفتوحة لأليس ولا يطلب مصادقة تفاعلية. ويحاول استرداد محتوى الصورة. وبذلك، يقوم المتصفح بتشغيل الرابط وحذف الرسالة، واسترداد صفحة ويب نصية كمحتوى للصورة. ونظرًا لأنه لا يتعرف على نوع الصورة المرتبطة، فإنه لا يعرض صورة، ولا تدرك أليس أن مالوري قد جعلتها للتو تحذف رسالة رغماً عنها.

حتى مع هذا الشرح، تظل تقنية CSRF صعبة الفهم. دعونا نرسم مخططًا توضيحيًا:

Image

  • في [1-2]، تتواصل أليس مع المنتدى (الموقع أ). يحافظ هذا المنتدى على جلسة لكل مستخدم. يخزن متصفح أليس ملف تعريف الارتباط الخاص بهذه الجلسة محليًا ويعيده في كل مرة يرسل فيها طلبًا جديدًا إلى الموقع أ؛
  • في [3]، ترسل مالوري رسالة إلى أليس. تقرأ أليس الرسالة في متصفحها. الرسالة بتنسيق HTML وتحتوي على رابط لصورة على الموقع ب. في الواقع، هذا الرابط هو رابط لبرنامج نصي JavaScript يتم تشغيله بمجرد وصوله إلى متصفح أليس؛
  • ثم يقوم نص JavaScript هذا بإرسال طلب إلى الموقع A. يرسل متصفح أليس الطلب تلقائيًا مع ملف تعريف الارتباط الخاص بالجلسة المخزن محليًا. وهنا يحدث الهجوم: نجحت مالوري في الوصول إلى الموقع A باستخدام بيانات اعتماد جلسة أليس. من هذه النقطة فصاعدًا، وبغض النظر عما يحدث، يكون الهجوم قد تم؛

لمواجهة هذا النوع من الهجمات، يمكن للموقع أ اتباع الخطوات التالية:

  • مع كل تبادل [1-2] مع أليس، يرسل الموقع A مفتاحًا، يُشار إليه فيما بعد باسم رمز CSRF، والذي يجب على أليس إرجاعه في طلبها التالي. وبالتالي، مع كل طلب، يجب على أليس إرسال معلومتين:
    • ملف تعريف ارتباط الجلسة؛
    • رمز CSRF الذي تم استلامه في الرد على طلبها الأخير إلى الموقع A؛

وهنا تكمن الحماية: في حين أن المتصفح يرسل ملف تعريف الارتباط للجلسة تلقائيًا إلى الموقع A، فإنه لا يفعل ذلك مع رمز 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 من نص الطلب؛
  • هناك تقنيات أخرى ممكنة؛

34.2. التكوين

Image

نضيف قيمتين منطقيتين إلى تكوين [parameters] للتطبيق:

  • [with_redissession]: عند تعيينه على True، يستخدم التطبيق جلسة Redis. وعند تعيينه على False، يستخدم التطبيق جلسة Flask قياسية؛
  • [with_csrftoken]: عند تعيينها إلى True، تحتوي عناوين URL الخاصة بالتطبيق على رمز CSRF؛

        # durée pause thread en secondes
        "sleep_time"0,
        # serveur Redis
        "with_redissession"True,
        "redis": {
            "host""127.0.0.1",
            "port"6379
        },
        # token 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:
        #  we encapsulate the paged data in the model
        modèle = {}
        

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

        #  we render the model
        return modèle
  • تمتد كل فئة نموذج من الفئة الأساسية [AbstractBaseModelForView]؛
  • السطر 8: يتم طلب رمز CSRF من الفئة الأصلية. نحصل إما على سلسلة فارغة أو سلسلة مثل [/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c]؛

34.3.3. طرق العرض

مما رأيناه للتو، ستحتوي جميع طرق العرض V على رمز CSRF في قالبها M. وبالتالي يمكنها استخدامه في الروابط التي تحتوي عليها. لنلقِ نظرة على بعض الأمثلة:

جزء المصادقة [v_authentification.html]


<!-- form HTML - post its values with the [authenticate-user] action -->
<form method="post" action="/authentifier-utilisateur{{modèle.csrf_token}}">
 
    <!-- title -->
    <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]


<!-- form HTML posted -->
<form method="post" action="/calculer-impot{{modèle.csrf_token}}">
    <!-- 12-column message on blue background -->
    <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 %}
<!-- message on blue background -->
<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 %}
<!-- message on blue background -->
<div class="alert alert-primary" role="alert">
    <h4>Liste de vos simulations</h4>
</div>
 
<!-- simulation table -->
<table class="table table-sm table-hover table-striped">
    
    <!-- table body (data displayed) -->
    <tbody>
    <!-- display each simulation by browsing the simulation table -->
    {% for simulation in modèle.simulations %}
 
    <!-- display a table row with 6 columns - <tr> tag -->
    <!-- column 1: row header (simulation no.) - <th scope='row' tag -->
    <!-- column 2: parameter value [married] - <td> tag -->
    <!-- column 3: parameter value [children] - <td> tag -->
    <!-- column 4: parameter value [salary] - <td> tag -->
    <!-- column 5: [tax] parameter value - <td> tag -->
    <!-- column 6: parameter value [surcôte] - <td> tag -->
    <!-- column 7: parameter value [discount] - <td> tag -->
    <!-- column 8: parameter value [reduction] - <td> tag -->
    <!-- column 9: parameter value [rate] (of tax) - <td> tag -->
    <!-- column 10: link to delete simulation - <td> tag -->
    <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 menu -->
<nav class="nav flex-column">
    <!-- display a list of 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. المسارات

يوجد الآن نوعان من المسارات، اعتمادًا على ما إذا كانت تستخدم رمز CSRF أم لا:

Image

  • [routes_without_csrftoken] هي مسارات بدون رمز CSRF. هذه هي المسارات من الإصدار السابق؛
  • [routes_with_csrftoken] هي مسارات تحتوي على رمز CSRF.

في [routes_with_csrftoken]، تحتوي المسارات الآن على معلمة إضافية، وهي رمز CSRF:

#  the front controller
def front_controller() -> tuple:
    #  forward the request to the main controller
    main_controller = config['mvc']['controllers']['main-controller']
    return main_controller.execute(request, session, config)

@app.route('/', methods=['GET'])
def index() -> tuple:
    #  redirect to /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:
    #  execute the controller associated with the action
    return front_controller()

#  authenticate-user
@app.route('/authentifier-utilisateur/<string:csrf_token>', methods=['POST'])
def authentifier_utilisateur(csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  calculate-tax
@app.route('/calculer-impot/<string:csrf_token>', methods=['POST'])
def calculer_impot(csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  batch tax calculation
@app.route('/calculer-impots/<string:csrf_token>', methods=['POST'])
def calculer_impots(csrf_token: str):
    #  execute the controller associated with the action
    return front_controller()

#  lister-simulations
@app.route('/lister-simulations/<string:csrf_token>', methods=['GET'])
def lister_simulations(csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  delete-simulation
@app.route('/supprimer-simulation/<int:numero>/<string:csrf_token>', methods=['GET'])
def supprimer_simulation(numero: int, csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  end of session
@app.route('/fin-session/<string:csrf_token>', methods=['GET'])
def fin_session(csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  display-calculation-tax
@app.route('/afficher-calcul-impot/<string:csrf_token>', methods=['GET'])
def afficher_calcul_impot(csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  get-admindata
@app.route('/get-admindata/<string:csrf_token>', methods=['GET'])
def get_admindata(csrf_token: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

تحتوي جميع المسارات الآن على رمز CSRF في معلماتها، بما في ذلك مسار [/init-session]. وهذا يعني أنه لا يمكن للعميل تشغيل التطبيق عن طريق كتابة عنوان URL [/init-session/html] مباشرةً لأن رمز CSRF سيكون مفقودًا. يجب عليه الآن المرور عبر عنوان URL [/] في الأسطر 7–10.

يتم تحديد المسارات في البرنامج النصي الرئيسي [main]:


#  the main thread no longer needs the logger
logger.close()

#  if there has been an error, we stop
if erreur:
    sys.exit(2)

#  import routes from web application
if config['parameters']['with_csrftoken']:
    import routes_with_csrftoken as routes
else:
    import routes_without_csrftoken as routes

#  route configuration
routes.config = config

#  start Flask application
routes.execute(__name__)
  • الأسطر 9–13: اختيار المسارات اعتمادًا على ما إذا كان التطبيق يستخدم رموز CSRF أم لا؛

34.3.5. [MainController]

لكل طلب، يجب على الخادم التحقق من وجود رمز CSRF. سنقوم بذلك في وحدة التحكم الرئيسية [MainController التي تتولى معالجة جميع الطلبات:

from flask_wtf.csrf import generate_csrf, validate_csrf

       #  we process the request
        try:
            #  logger
            logger = Logger(config['parameters']['logsFilename'])

            

            #  path elements are retrieved
            params = request.path.split('/')

            #  action is the 1st element
            action = params[1]

            

            if config['parameters']['with_csrftoken']:
                #  the csrf_token is the last element of the path
                csrf_token = params.pop()
                #  check token validity
                #  an exception will be thrown if the csrf_token is not the expected one
                validate_csrf(csrf_token)

            

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

        except BaseException as exception:
            #  other (unexpected) exceptions
            résultat = {"action": action, "état": 131, "réponse": [f"{exception}"]}
            status_code = status.HTTP_400_BAD_REQUEST

        finally:
            pass

        #  add the csrf_token to the result
        résultat['csrf_token'] = generate_csrf()

        #  we log the result sent to the customer
        log = f"[MainController] {résultat}\n"
        logger.write(log)
  • السطر 20: استرداد رمز CSRF من عنوان URL لطلب النموذج [http://machine:port/path/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 يكون صالحًا إذا تم إنشاؤه أثناء الجلسة. وبالتالي، إذا قمت باستبدال رمز CSRF [xyz] يدويًا في عنوان URL المعروض في المتصفح — على سبيل المثال، (/lister-simulations/xyz) — برمز آخر [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 التي نفذها المتصفح، مثل [/target/csrf_token]. يمكنه بعد ذلك استرداد رمز الجلسة [csrf_token] وتنفيذ هجومه في [6-7]. ومع ذلك، لا يسمح المتصفح بالوصول إلا إلى سجل نافذة المتصفح التي يعمل فيها البرنامج النصي. لذلك، إذا لم تستخدم أليس نفس النافذة للتفاعل مع الموقع A [1-2] وقراءة رسالة مالوري [3]، فلن يكون هجوم CSRF ممكنًا.

34.5. عملاء وحدة التحكم

هناك طريقة أخرى لاختبار الإصدار 14 من التطبيق وهي إعادة استخدام الاختبارات من الإصدار 12 وتكييفها مع الخادم الجديد.

Image

يتم إنشاء المجلد [impots/http-clients/09] في البداية عن طريق نسخ المجلد [impots/http-clients/07]. ثم يتم تعديله.

لنعد إلى المسارات التي تبدأ الجلسة:

#  application root
@app.route('/', methods=['GET'])
def index() -> tuple:
    #  redirect to /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:
    #  execute the controller associated with the action
    return front_controller()

لا تصلح أي من هذه المسارات لتهيئة جلسة JSON أو XML:

  • الأسطر 2–5: المسار [/] يقوم بتهيئة جلسة عمل HTML؛
  • الأسطر 8–11: المسار [/init-session] يتطلب رمز CSRF لا نعرفه؛

قررنا إضافة مسار جديد إلى الخادم:

1
2
3
4
5
#  init-session-without-csrftoken
@app.route('/init-session-without-csrftoken/<string:type_response>', methods=['GET'])
def init_session_without_csrftoken(type_response: str) -> tuple:
    #  redirect to /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({
        # fichier des contribuables
        "taxpayersFilename"f"{script_dir}/../data/input/taxpayersdata.txt",
        # fichier des résultats
        "resultsFilename"f"{script_dir}/../data/output/résultats.json",
        # fichier des erreurs
        "errorsFilename"f"{script_dir}/../data/output/errors.txt",
        # fichier de logs
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # le serveur de calcul de l'impôt
        "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",
            }
        },
        # mode debug
        "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'
  • السطر 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

#  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):

    #  manufacturer
    def __init__(self, config: dict):
        #  parent initialization
        AbstractImpôtsDao.__init__(self, config)
        #  saving configuration items
        #  general configuration
        self.__config = config
        #  server
        self.__config_server = config["server"]
        #  services
        self.__config_services = config["server"]['url_services']
        #  debug mode
        self.__debug = config["debug"]
        #  logger
        self.__logger = None
        #  cookies
        self.__cookies = None
        #  session type (json, xml)
        self.__session_type = None
        #  token CSRF
        self.__csrf_token = None

    # étape request / response
    def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
        #  [method]: HTTP GET or POST method
        #  [url_service] : URL of service
        #  [data]: POST parameters in x-www-form-urlencoded
        #  [json]: POST parameters in json
        #  [cookies]: cookies to include in the request

        #  you must have a XML or JSON session, otherwise you won't be able to handle the response
        if self.__session_type not in ['json', 'xml']:
            raise ImpôtsError(73, "il n'y a pas de session valide en cours")

        #  we add the CSRF token to the URL service token
        if self.__csrf_token:
            url_service = f"{url_service}/{self.__csrf_token}"

        #  query execution
        response = requests.request(method,
                                    url_service,
                                    data=data_value,
                                    json=json_value,
                                    cookies=self.__cookies,
                                    allow_redirects=True)

        #  debug mode?
        if self.__debug:
            #  logger
            if not self.__logger:
                self.__logger = self.__config['logger']
            #  log on
            self.__logger.write(f"{response.text}\n")

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

        #  retrieve response cookies, if any
        if response.cookies:
            self.__cookies = response.cookies

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

        #  status code
        status_code = response.status_code

        #  if status code other than 200 OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(35, résultat['réponse'])

        #  we return the result
        return résultat['réponse']


    def init_session(self, session_type: str):
        #  note the session type
        self.__session_type = session_type

        #  delete the CSRF token from previous calls
        self.__csrf_token = None

        #  the URL of the init-session action is requested
        url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        #  request execution
        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]، سيتبع هذه الإعادة التوجيه [طلبات] من العميل؛
  • سيتم العثور على رمز CSRF في النتيجة المستردة في السطرين 72 و 74 المرتبطين بالمفتاح [csrf_token]؛

عند التواجد في وضع [with_csrftoken=False]:

  • (تابع)
    • يبدأ العملاء تفاعلهم مع الخادم باستدعاء المسار [/init-session/type_response]؛
    • يستجيب الخادم لهذا الطلب بإعادة توجيه إلى المسار [/init-session/type_response]؛
    • بسبب المعلمة [allow_redirects=True]، سيتبع هذه الإعادة التوجيه [طلبات] من العميل؛
    • لا يوجد رمز CSRF لاسترداده في السطور 81-82. وبالتالي تظل الخاصية [self.__csrf_token] None (السطر 36)؛
  • السطران 51–52: بالنسبة لجميع الطلبات اللاحقة، يتم إضافة رمز CSRF، إن وجد، إلى المسار الأولي؛
  • السطران 81–82: يتم تخزين الرمز الجديد الذي تم إنشاؤه بواسطة الخادم لكل طلب عميل جديد محليًا ليتم إرجاعه في السطر 52 مع الطلب التالي؛

بالإضافة إلى ذلك، تتغير طريقة [init_session] قليلاً:

    def init_session(self, session_type: str):
        #  note the session type
        self.__session_type = session_type

        #  delete the CSRF token from previous calls
        self.__csrf_token = None

        #  the URL of the init-session action is requested
        url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        #  request execution
        self.get_response("GET", url_service)

من المهم أن نتذكر هنا أننا أنشأنا مسارًا [/init-session-without-csrftoken/<response-type>] لبدء الحوار بين العميل والخادم بدون رمز CSRF. ومع ذلك، فقد رأينا أن طريقة [get_response] التي تم استدعاؤها في السطر 12 من الكود تضيف بشكل منهجي رمز CSRF المخزن في [self.__csrf_token] إلى نهاية عنوان URL للخدمة. ولهذا السبب، في السطر 6 من الكود، نقوم بإزالة رمز CSRF هذا إذا كان موجودًا.

هذا كل شيء. للاختبار، سنقوم بتشغيل:

  • the console clients [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 التي تم استلامها بالتتابع، نرى أنها جميعها مختلفة.