angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Flask, WTForms und AJAX: CSRF Schutz, before_request und mehrsprachig

Sie sollten immer prüfen, ob der Schutz von CSRF funktioniert. Bei Flask ist dies nicht offensichtlich.

29 Februar 2020
post main image
https://unsplash.com/@christineashleydonaldson

Ich habe nie wirklich überprüft, ob der Schutz von CSRF in meiner Anwendung Flask , dieser Website, funktioniert. Ist sie standardmäßig aktiviert? Aus der Dokumentation zur Erweiterung Flask_WTF:

Jede Ansicht, die FlaskForm zur Bearbeitung der Anfrage verwendet, erhält bereits den Schutz von CSRF .

Und aus dem Text von Miguel Grinbergs Beitrag 'Cookie-Sicherheit für Flask -Anwendungen':

Wenn Sie Ihre Web-Formulare mit der Erweiterung Flask-WTF bearbeiten, sind Sie bereits standardmäßig gegen CSRF auf Ihren Formularen geschützt.

Sie sollte aktiviert werden, prüfen wir, ob dies wahr ist. Auf der Seite mit dem Formular, das ich hinzugefügt habe:

<script>
$(document).ready(function(){
    $('#csrf_token').val('ABC');
});
</script>

Dann habe ich das Formular aufgefrischt. Im Debugger verifizierte ich, dass sich das csrf_token in 'ABC' geändert hat. Das Klicken auf die Schaltfläche 'Submit' sollte einen Fehler CSRF ergeben, nun, ich erwartete eine Ausnahme CSRF . Für mich gibt es keine Ausnahme, d.h. keinen CSRF -Schutz. Wie ist das möglich? Hatte es damit zu tun, dass ich DispatcherMiddleWare benutze, oder stimmt etwas anderes nicht?

Ja, irgendwas stimmte nicht. Ich! Und es wurde durch TL;DR. Es gab tatsächlich einen Fehler CSRF , aber keine Ausnahme CSRF . Ich habe viele Formulare, hauptsächlich im Admin-Bereich, und sie haben die ganze Zeit mit dem CSRF -Schutz gearbeitet, ohne dass ein CSRF -Fehler auftrat. Was geht hier vor?

Mein Testapp:

def  create_app():
    fname = 'create_app'
    print(fname  +  '()')

    app =  Flask(__name__)

    app.config['SECRET_KEY'] = 'some-secret-key'

    class MyCSRFForm(FlaskForm):
        submit = SubmitField(_l('Send'))

    @app.route('/csrf_form', methods=['GET', 'POST'])
    def csrf_form():
        fname = 'csrf_form'
        print(fname  +  '()')

        form = MyCSRFForm()
        if form.validate_on_submit():
            print(fname  +  ': no validate_on_submit errors, processing form')

        print(fname  +  ': form.errors = '.format(form.errors))
        for field, errors in form.errors.items():
            print(fname  +  ': form.errors, field = {}, errors = {}'.format(field, errors))
                
        return  render_template('csrf_form.html', form=form)

und die Vorlage csrf_form.html:

<html>
<head></head>
<body>

<form method="post">
    {{ form.csrf_token }}
    <p>{{ form.submit() }}</p>
</form>

<script>
elem = document.getElementById('csrf_token');
elem.setAttribute('value', 'ABC'); 
</script>

</body>
</html>

Das Ergebnis:

csrf_form: form.errors, field = csrf_token, errors = ['The  CSRF  token is invalid.']

Weitere Informationen dazu finden Sie unter 'Inkonsistenz bei der Auslösung von CSRFError #381', siehe die Links unten. Es scheint, dass Sie den Schutz von Flask-WTF auf zwei Arten verwenden können:

  • als CSRF -Fehlermeldung bei der Formular-Validierung
  • als Ausnahme CSRF

Beide haben Vor- und Nachteile. Ich habe mich für die Ausnahme CSRF entschieden, weil man das nicht versehentlich vergessen kann. Um die Ausnahmen für CSRF zu generieren, habe ich den vorgeschlagenen Code in meiner __init__.py hinzugefügt:

from flask_wtf.csrf import  CSRFProtect
csrf =  CSRFProtect()
...

def  create_app(project_config):
    ...
    # csrf protection
    csrf.init_app(app)

Wenn ich jetzt das Formular einreiche, erhalte ich eine Ausnahme CSRF :

Bad Request
The  CSRF  token is invalid.

Ich änderte die jQuery und entfernte den Feldnamen csrf_token:

<script>
$(document).ready(function(){
    $('#csrf_token').attr({name: 'nothing'});
});
</script>

Aktualisieren und senden, die Ausnahme ist CSRF :

Bad Request
The  CSRF  token is missing.

Und als letzten Test entfernen wir das Skript und setzen die Ablaufzeit des Token CSRF in create_app() auf 5 Sekunden:

    app.config['WTF_CSRF_TIME_LIMIT'] = 5

Die Ausnahme ist CSRF :

Bad Request
The  CSRF  token has expired.

Zusammenfassung dieser Tests:

  • Der http -Fehlercode lautet 400 Bad Request
  • Es schien, dass der CSRF -Schutz standardmäßig funktioniert und auf zwei Arten implementiert werden kann, die Dokumentation ist diesbezüglich nicht sehr klar
  • Ich wollte CSRF -Ausnahmen und fügte den vorgeschlagenen Zusatzcode
  • Es war sehr einfach, Ausnahmen für die Bedingungen CSRF zu generieren:
    - Das CSRF -Token ist ungültig
    - Das CSRF -Token fehlt
    - Das CSRF -Token ist abgelaufen

Mehrere Formulare und das CSRF -Token

Eine Blog-Post-Seite auf dieser Website hat zwei Arten von comment forms: die comment form und die comment reply form, siehe auch einen früheren Post.
Bei der Betrachtung der Seite mit den Token comment form und comment reply form fiel mir auf, dass beide nach dem Laden einer Seite den Token-Wert CSRF hatten.

comment form: 
<input id="comment_form-csrf_token" name="comment_form-csrf_token" type="hidden" value="IjM4N2Y3NmEzMGFkNGMwOTY2MzM5OGI0NTVjOGQwNDI3MjY3MmQxOGIi.Xk6ReA.dFbMlM0bClY6BvAVjxZ0GfiFBM4">

comment reply form: 
<input id="comment_reply_form-csrf_token" name="comment_reply_form-csrf_token" type="hidden" value="IjM4N2Y3NmEzMGFkNGMwOTY2MzM5OGI0NTVjOGQwNDI3MjY3MmQxOGIi.Xk6ReA.dFbMlM0bClY6BvAVjxZ0GfiFBM4">

oder:

comment_form-csrf_token: IjM4N2Y3NmEzMGFkNGMwOTY2MzM5OGI0NTVjOGQwNDI3MjY3MmQxOGIi.Xk6ReA.dFbMlM0bClY6BvAVjxZ0GfiFBM4

comment_reply_form-csrf_token: IjM4N2Y3NmEzMGFkNGMwOTY2MzM5OGI0NTVjOGQwNDI3MjY3MmQxOGIi.Xk6ReA.dFbMlM0bClY6BvAVjxZ0GfiFBM4

Dann übermittle ich eine zu kurze Nachricht mit dem comment reply form. Das Formular wird an den Server gesendet, auf dem Server gerendert und an den Client zurückgeschickt.
Die Inspektion ergab, dass sich das CSRF -Token des comment reply form geändert hat:

comment_reply_form-csrf_token: IjM4N2Y3NmEzMGFkNGMwOTY2MzM5OGI0NTVjOGQwNDI3MjY3MmQxOGIi.Xk6SUg.6axMGCN2KmQQ4WeAAxYgTg6UdAs

Hier kamen die folgenden Fragen auf:

  • Wie ist das CSRF -Token aufgebaut?
  • Wann läuft das CSRF -Token ab?
  • Benötigen verschiedene Formulare auf einer Seite unterschiedliche CSRF -Token?
  • Wie kann sich das CSRF -Token des comment reply form ändern?
  • Was ist das X-CSRFToken?

Wie ist das CSRF -Token aufgebaut?

Zeit, wieder mit dem Lesen zu beginnen. Aus dem Leitfaden OWASP :

Im Allgemeinen müssen Entwickler dieses Token nur einmal für die aktuelle Sitzung generieren. Nach der anfänglichen Generierung dieses Tokens wird der Wert in der Sitzung gespeichert und für jede nachfolgende Anforderung verwendet, bis die Sitzung abläuft.

In Flask-WTF befindet sich das Roh-Token in der Sitzung und das signierte Token in g. Oder genauer gesagt, das rohe Token ist immer in der Sitzung und das signierte Token ist in g, nachdem ein Formular verwendet wurde. Vor der Instanziierung eines Formulars:

session['csrf_token']: 387f76a30ad4c09663398b455c8d04272672d18b
g.csrf_token:  None

Nach der Instanziierung eines Formulars:

session['csrf_token']: 387f76a30ad4c09663398b455c8d04272672d18b
g.csrf_token: IjM4N2Y3NmEzMGFkNGMwOTY2MzM5OGI0NTVjOGQwNDI3MjY3MmQxOGIi.Xk6vnw.L7uNOptXpBxemh_TIEy3e9Ujllg

Das signierte Token wird in g gesetzt, so dass bei nachfolgenden Anfragen, z.B. bei mehreren Formularen, das gleiche signierte Token verwendet werden kann und nicht erneut generiert werden muss. Wenn Sie möchten, können Sie mit den Funktionen generate_csrf() und validate_csrf() mit der Generierung und Validierung von Token experimentieren:

    from flask_wtf.csrf import generate_csrf, validate_csrf

    secret_key =  current_app.config['SECRET_KEY']
    # default: WTF_CSRF_FIELD_NAME = 'csrf_token'
    token_key =  current_app.config['WTF_CSRF_FIELD_NAME']

    csrf_token = generate_csrf(secret_key=secret_key, token_key=token_key)
     current_app.logger.debug(fname  +  ': csrf_token = {}'.format(csrf_token))

    validated = False
    try:
        validate_csrf(csrf_token, secret_key=secret_key, time_limit=3600, token_key=token_key)
        validated = True
    except:
        pass
     current_app.logger.debug(fname  +  ': validated = {}'.format(validated))

Die oben genannten Funktionen machen so etwas wie eine Black Box. Aber wir können sehen, dass das generierte Token signiert ist und einen Zeitstempel enthält.

Wann läuft das CSRF -Token ab?

Wir haben bereits gesehen, dass die Ablaufzeit des CSRF -Tokens mit gesteuert werden kann:

    app.config['WTF_CSRF_TIME_LIMIT'] = 5

Der Standardwert ist 3600, eine Stunde. Diese Zeit ist irgendwo in der Marke. Es gibt zwei Möglichkeiten, wie dies umgesetzt werden kann:

  • Die Startzeit ist im Token CSRF
  • Die Endzeit ist im CSRF -Token

Dies ist nicht wichtig zu wissen, es sei denn, Sie möchten die Ablaufzeit in Ihrem Antrag ändern. Die Funktion generate_csrf() enthält keinen Parameter time_limit, während die Funktion validate_csrf einen solchen enthält. Dies würde bedeuten, dass die Startzeit im CSRF -Token steht. Um dies zu überprüfen, habe ich eine Seite mit einem Formular auf den Bildschirm gebracht, dann die Standardzeitbegrenzung auf 10 Sekunden geändert und das Formular dann abgeschickt. Wie erwartet wurde eine Ausnahme für CSRF ausgelöst.

Es gibt eine weitere Bedingung, wenn Ihr CSRF -Token abläuft, und zwar wenn die Sitzung abläuft. Der Grund dafür ist, dass das CSRF -Roh-Token in der Sitzung gespeichert wird. Das bedeutet, dass Sie Fehler für CSRF erhalten können, wenn Ihre Sitzung kürzer als die Ablaufzeit Ihres CSRF -Tokens ist.

Benötigen verschiedene Formulare auf einer Seite unterschiedliche CSRF -Token?

Antwort: Dies ist eine eher allgemeine Frage. Die Antwort ist nein. Wir können das gleiche CSRF -Token für alle Formulare auf einer Seite verwenden.

Wie kann sich das CSRF -Token des comment reply form ändern?

Antwort: Das Token ändert sich nicht, aber das signierte Token ändert sich, weil es auch einen Zeitstempel im signierten Token gibt. Wenn Sie sich die signierten Token oben ansehen, stellen Sie fest, dass der erste Teil identisch ist.

Der Header-Parameter X-CSRFToken

Sie haben vielleicht über den X-CSRFToken gelesen. Das X-CSRFToken ist eine Einstellung in der Kopfzeile. Normalerweise brauchen wir das nicht. Wenn Sie einen Token-Parameter CSRF in Ihrem AJAX POST haben, wird dieser verwendet. Wenn nicht, dann kann dies in der Kopfzeile der POST mit jQuery eingestellt werden:

var csrf_token = ...

    ...
    $.ajax({
        beforeSend: function(xhr, settings){
            if(!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain){
                xhr.setRequestHeader("X-CSRFToken", csrf_token);
            }
        },

Es hängt von Ihrer Implementierung ab, ob Sie Formulare mit einem CSRF -Token-Parameter senden oder ob Sie den X-CSRFToken-Header-Parameter setzen. In der Dokumentation steht, dass Sie dies nur im 'CSRF Ausnahmemodus' (Hinzufügen des Extra-Codes) verwenden können. Ich habe das nicht überprüft.

Behandlung der Ausnahmen CSRF und before_request

Ich behandle bereits HTTP-Fehlerausnahmen wie 401 (nicht autorisiert), 403 (verboten), 404 (nicht gefunden), 405 (nicht erlaubte Methode), 500 (interner Serverfehler), aber wie behandeln wir die Ausnahme CSRF ? Der Fehler CSRF breitet sich auf einen 400 (Bad Request) aus.

Ich habe nette Schnickschnack-Fehlerseiten erstellt, die die Ausnahmen zeigen. Ich tat dies, indem ich mich auf die Tatsache stützte, dass before_request aufgerufen wurde.
In before_request bearbeite ich die eingehende Anfrage und stelle unter anderem die ausgewählte Sprache entsprechend ein.

Leider wird standardmäßig before_request NICHT bei einer CSRF -Ausnahme aufgerufen. Aber es gibt einen Weg, dies zu umgehen. Wir können setzen:

    app.config['WTF_CSRF_CHECK_DEFAULT'] = False

Das Ergebnis ist, dass before_request aufgerufen wird. Dann können wir in before_request, nach einigen wesentlichen Bearbeitungen, anrufen:

     csrf.protect()

Dies löst eine CSFR-Ausnahme aus, falls es eine gibt, und ruft den Fehlerbehandler entsprechend auf.

Mehrsprachige CSRF Ausnahmemeldungen

Ich verwende Flask-Babel und die Validierungsmeldungen werden in die ausgewählte Sprache übersetzt. Die CSRF -Ausnahmemeldungen wurden nicht übersetzt, und bei der Überprüfung von csrf.py stellte sich heraus, dass diese Meldungen fest in Englisch(?) kodiert sind. Für den Moment habe ich ein Wörterbuch erstellt, um die Übersetzung zu handhaben:

    csrf_error_message2translated_error_messages = {
        ...
        'The  CSRF  token has expired.': _('The  CSRF  token has expired.'),
        ...
    }
    if error.description in error_description2error_messages:
        error_message = error_description2error_messages[error.description]
    else:
        error_message = error.description

Beantwortung von Ausnahmen, einschließlich CSRF, bei AJAX -Anfragen

Bei AJAX können wir unsere schöne Fehlerseite HTML nicht auf einer Ausnahme zeigen. Auf eine Anfrage hin müssen wir einen Fehlercode oder HTML zurückgeben, den der Kunde versteht. Es läuft darauf hinaus, die Ausnahme als JSON kodierte Fehlermeldung an den Client zurückzugeben. In Flask haben wir bereits unsere Fehlerbehandler.
Aber woher wissen wir, dass die Anfrage von einem AJAX Anruf kam? Ich sende das Formular mit der Kodierung application/x-www-form-urlencoded:

    $.ajax({
        data: $('#'  +  form_id).serialize()   +  '&'  +  encodeURI( $(submit_button).attr('name') )  +  '='  +  encodeURI( $(submit_button).val() ),
        type: 'POST',
        url: url_comment_new,
        contentType: 'application/x-www-form-urlencoded; charset=UTF-8', 
        dataType: 'json'
    })

Eine Möglichkeit besteht darin, sich den empfangenen Accept-Header anzusehen.

     current_app.logger.error(fname  +  ': request.accept_mimetypes = {}'.format(request.accept_mimetypes))
    # text/javascript,application/json,*/*;q=0.01

Aus dem Dokument 'Liste der Standard-Akzeptanzwerte':

Beachten Sie, dass alle Browser den */* MIME-Typ hinzufügen, um alle Fälle abzudecken. Dies wird typischerweise für Anfragen verwendet, die über die Adressleiste eines Browsers oder über ein HTML <a> Element initiiert werden.

Dies bedeutet, dass sowohl 'request.accept_mimetypes.accept_json' als auch 'request.accept_mimetypes.accept_html' wahr sind. Nicht sehr nützlich. Die accept_mimetypes sind in der Reihenfolge ihrer Präferenz aufgeführt. Wir könnten die Liste einscannen und entscheiden, JSON zurückzugeben, wenn wir auf 'application/json' vor 'text/html' stoßen. Aber ich weiß nicht genau, welche Browser welche accept_mimetypes senden, d.h. ich bin im Moment nicht 100% sicher, dass die Fehlerseiten 'ohne AJAX' nicht versehentlich als JSON angezeigt werden.

Eine viel sicherere Methode ist es, einen Parameter zu AJAX -Requests hinzuzufügen und in der Fehlerbehandlung zu prüfen, ob dieser Parameter vorhanden ist. Wenn sie vorhanden ist, geben wir eine JSON -Fehlermeldung zurück, wenn sie nicht vorhanden ist, geben wir die Standardfehlerseite aus. Mir gefällt das nicht, aber es funktioniert, und mit der Zeit wird man sich noch tiefer in die Materie einarbeiten.

Zusammenfassung

Sie sollten immer prüfen, ob der Schutz von CSRF funktioniert. Ich habe es nicht getan, und das war der Beginn einer sehr umfangreichen Lektüre. Es zeigte sich, dass es zwei Möglichkeiten gibt, mit dem CSRF -Schutz, dem CSRF -Formularfehler und der CSRF -Ausnahme umzugehen. Ich wähle den zweiten. Für mich war die Behandlung des Schutzes von CSRF für diese mehrsprachige Website nicht Standard, da before_request nicht standardmäßig bei einer Ausnahme von CSRF aufgerufen wird. In before_request prüfe und stelle ich unter anderem die Sprache ein, damit dies wirklich laufen muss. Glücklicherweise können wir den Schutz von CSRF verzögern und ihn nach der wesentlichen Bearbeitung mit csrf.protect() explizit in before_request aufrufen. Wir haben bereits unsere Ausnahmefehlerseiten, aber bei AJAX -Anfragen müssen wir JSON -Fehlermeldungen zurückgeben. Das Erkennen, ob die Anfrage von einer AJAX -Anfrage kommt, ist komplex, daher habe ich einen zusätzlichen Parameter in den AJAX -Anfragen verwendet.

Links / Impressum

Cookie Security for Flask Applications
https://blog.miguelgrinberg.com/post/cookie-security-for-flask-applications

Cross-Site Request Forgery (CSRF) Prevention Cheat Sheet
https://owasp.org/www-project-cheat-sheets/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#Prevention_Measures_That_Do_NOT_Work

Flask-WTF CSRF Protection
https://flask-wtf.readthedocs.io/en/latest/csrf.html

Generating CSRF tokens for multiple forms on a single page
https://stackoverflow.com/questions/14715250/generating-csrf-tokens-for-multiple-forms-on-a-single-page

How to use Flask-WTForms CSRF protection with AJAX?
https://stackoverflow.com/questions/31888316/how-to-use-flask-wtforms-csrf-protection-with-ajax

Inconsistency with raising CSRFError #381
https://github.com/lepture/flask-wtf/issues/381

List of default Accept values
https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation/List_of_default_Accept_values

Posting a WTForm via AJAX with Flask
https://medium.com/@doobeh/posting-a-wtform-via-ajax-with-flask-b977782edeee

Unacceptable Browser HTTP Accept Headers (Yes, You Safari and Internet Explorer)
https://www.newmediacampaigns.com/blog/browser-rest-http-accept-headers

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare (1)

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.

avatar

Thanks for placing this post.
I have a problem with my webapp that raises CSRF errors, which I asked in
https://stackoverflow.com/questions/65985400/getting-csrf-errors-from-put-fetch-request-that-does-not-involve-flask-forms
Can you please explain why I'm getting CSRF error in the first place, and how to fix the problem?
Thanks again.
Avner