ZURÜCK

Invalidate your CSRF tokens!

Symfony CSRF Token Test

Datum: 2022-06, Tags: php, symfony, security, test, csrf, token, xss

The sources are available on gitlab: https://gitlab.com/limitland/csrf-token-test.


Die Symfony Forms sind sehr weit verbreitete Komponenten zur Behandlung von HTML Formularen in PHP. Die Formulare werden auf Basis von Formulartypen und Optionen konfiguriert und beim Empfang der Formular-Daten auf Datenklassen gemapped und validiert. Unter der Haube gibt es noch ein Event-System sowie Pre- und Post-Prozessoren und viele andere Schnittstellen und Integrationen. Die Symfony Forms haben sich als sehr gut durchdacht und sehr flexibel herausgestellt, zumindest aus der Sichtweise der Backend-Entwicklung, weshalb sie auch gerne in anderen frameworks integriert werden.

Ein bekannter und beliebter Umstand ist, dass die Symfony Forms automatisch eine CSRF Protection über einen CSRF Token mitbringen. Das feature ist auch standardmäßig eingeschaltet, sodass in jedem Formular automatisch ein CSRF Token als hidden input mit auftaucht, etwa so:

<input type="hidden" id="formname__token" name="formname[_token]" value="some-random-string">

Dieser Token wird automatisch übermittelt, ausgewertet und validiert. Im Fehlerfall wird automatisch eine Benachrichtigung ausgegeben und das als invalid gekennzeichnete Formular wird nicht weiter verarbeitet. Sehr schön, so soll es sein!

Doch was ist das: Man kann ein erfolgreich übermittelte Formular problemlos ein weiteres mal übertragen oder sogar beliebig viele male!

csrftokentest-1

Wie kann das sein, denn genau so einen Fall sollte doch der CSRF Token abfangen? Was hebelt hier die Token-Validierung aus?

Test Szenario

Für den Test soll ein einfaches Symfony Formular verwendet werden, dass neben ein Paar einfachen Formularfeldern auch den CSRF Token enthalten soll. Der komplette sourcecode für den Test ist auf gitlab abgelegt: https://gitlab.com/limitland/csrf-token-test. Mit dem Formular möchte ich ein paar Fragen zu klären versuchen:

Für den Test installiere ich ein minimales Symfony framework, mit den beiden zusätzlichen Komponenten symfony/twig-bundle, denn die Generierung der CSRF Tokens erfolg über ein twig plugin zur Laufzeit des form-renderns, und natürlich symfony/form und symfony/security-csrf. Dann baue ich mir eine route, einen Controller, eine Form-Entity und einen FormType und stelle die benötigten templates zusammen. Wer sich für die Details interessiert, findet die wie gesagt unter https://gitlab.com/limitland/csrf-token-test. Der Controller sieht jedenfalls so aus:

CsrfTokenTestController.php
<?php
namespace App\Controller;

use App\Entity\Tokentest;
use App\Exception\InvalidCsrfTokenException;
use App\Form\Type\TokentestType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage;

class CsrfTokenTestController extends AbstractController
{
    /**
     * @param Request $request
     * @param CsrfTokenManagerInterface $tokenManager
     * @param SessionInterface $session
     *
     * @return Response
     *
     * @throws InvalidCsrfTokenException
     */
    public function home(Request $request, CsrfTokenManagerInterface $tokenManager, SessionInterface $session): Response
    {
        $submittedToken = '';

        $entity = new Tokentest();
        $entity->setSomeText('default text');
        $entity->setAcceptTerms(true);

        $form = $this->createForm(TokentestType::class, $entity);
        $form->handleRequest($request);

        if ($form->isSubmitted()) {
            $submittedToken = $request->request->get(TokentestType::TOKEN_ID)[TokentestType::TOKEN_NAME] ?? 'invalid';
            if (!$this->isCsrfTokenValid(TokentestType::TOKEN_ID, $submittedToken)) {
                throw new InvalidCsrfTokenException(sprintf(
                    'Invalid CSRF Token error: %s',
                    $form->getErrors()->current()->getMessage()
                ));
            }
        }

        $data = [
            'submitted_token' => $submittedToken,
            'session_token' => $session->get(SessionTokenStorage::SESSION_NAMESPACE . '/' . TokentestType::TOKEN_ID),
        ];

        return $this->renderForm('home/form.html.twig', [
            'form' => $form,
            'data' => $data,
        ]);
    }
}

Dazu vielleicht noch ein paar Anmerkungen:

Nun kann ich meine app mit symfony serve starten, die dann unter http://127.0.0.1:8000 erreichbar ist und etwa so aussieht:

csrftokentest-2

Dargestellt wird oben lediglich das Formular im Rohzustand. Das Formular wird mit den Submit button per POST wieder an den aufrufenden Controller übertragen.

Unterhalb der Trennlinie werden noch die relevanten Informationen ausgegeben, sodass ich nicht jedes Mal in den HTML-Quellcode schauen muss:

Der Session Token ist natürlich erst einmal leer, wenn die Seite das erste Mal aufgerufen wird. Nach einem Reload der Seite existiert jedoch schon einen Session-Token. Dieser ändert sich auch nicht, solange wir die Session offen halten, bzw. der Session Cookie im Browser gültig ist.
Nur dass überhaupt eine Session angelegt wird ist etwas verwunderlich, denn auf den ersten Blick gibt es überhaupt keine Stelle in dem Code, durch den eine Session gestartet werden würde oder müsste.

Doch hier kommt der CSRF Token ins Spiel. Wenn das Formular gerendert werden soll, prüft eine für den CSRF Token zuständige Erweiterung auf die Session, ob diese bereits gestartet ist und startet sie ansonsten. Daraufhin wird der CSRF Token in der Session gespeichert und mit in das Formular gerendert. Das geschieht übrigens auch schon bei dem ersten Request, nur sehen wir dann den "Session token" noch nicht, weil er erst zur Laufzeit des Seiten-Renderns generiert wird. Im Quellcode findet sich:

<input type="hidden" id="tokentest__token" name="tokentest[_token]" value="f53af76b.B77bJz7l-yPUUl1d7SisdefgWhg5K8HjBv2XG3gw7eU.ZvCWdwmMvWHiASoUpmPBBr-sCC1-G4zOdbv4dQJho6Q-5OxmCdW8YIQiDA">

Wenn wir nun die Seite ganz normal ein paar mal neu laden stellen wir fest:

Das gleiche stellen wir fest, wenn das Formular per Submit button und per POST request übertragen wird. Dann ist natürlich der "Submitted token" gleich dem vorherigen "Current token". Nach Übermittlung des Formulars sieht die Seite nun so aus:

csrftokentest-3

Und wir können, wie z.B. mit den browser developer tools wie im ersten screenshot, ein vorher schon einmal übermitteltes Formular erneut übertragen. Also einen zuvor schon einmal benutzten CSRF Token noch einmal benutzen! Das ist für mich unerwartet, denn ich gehe davon aus, dass ein CSRF token nur einmal benutzt werden kann. Resultat: Das Formular ist offenbar angreifbar und lässt sich eventuell zum Ausspionieren der dahinterliegenden Verarbeitung nutzen. Das lässt sich auch gut scripten, z.B. mit curl, siehe unten.

One Time vs. Session Token

Um das vorwegzunehmen: Mein bisheriges Verständnis eines CSRF Tokens ist ein one-time token. Also eine einzigartige Zeichenfolge, die nach einmaliger Benutzung und Validierung verfällt und unbrauchbar wird. Es gibt jedoch noch eine Variante dazu, nämlich ein sitzungsbasierender token. Dieser bleibt für die Sitzung oder Session des Benutzers valide und verfällt erst mit der Session (mit dem Session-Cookie, siehe oben). Auf den englischen Wikipedia-Seiten zu Cross-site request forgery gibt es einen Absatz im Abschnitt "Synchronizer token pattern", der das beschreibt:

„As the token is unique and unpredictable, it also enforces proper sequence of events (e.g. screen 1, then 2, then 3) which raises usability problem (e.g. user opens multiple tabs). It can be relaxed by using per session CSRF token instead of per request CSRF token.“

Mit sitzungsbasierenden Tokens kann man also erst einmal grundlegend einige Problematiken lösen, die insbesondere mit AJAX requests auftreten: Die Sequenz, in der die Tokens verarbeitet werden, muss immer gewährleistet bleiben. Eine parallele oder asynchrone Verarbeitung mehrerer oder vieler Request kann nicht sichergestellt werden.

Ein Ajax-Request mit einem one-time token muss immer auch einen neuen token als Rückgabe-Wert enthalten, der dann in die Datenlogik aufgenommen werden muss. Beispielsweise würde er in einer javascript-Variablen zur Laufzeit abgelegt. Dabei ist der neue Token transparent, auch für einen Angreifer oder Bot. Eigentlich macht es bis auf eine kleine technische Hürde für einen Angreifer keinen Unterschied, ob ein neuer Token generiert wurde, oder der "alte" einfach weiterbenutzt wird.

Zurück zu dem Formular und den Symfony CSRF Tokens.
Der CSRF Token, der in das Formular gerendert wird, der "Current token", entspricht nicht direkt dem Token, der in der Benutzersitzung gespeichert ist. Es ist eine reversible Verschlüsselung des eigentlichen Tokens, basierend auf einem Zufallswert, so dass der entschlüsselte Wert mit dem Token in der Benutzersitzung verglichen werden kann. In dem Symfony CsrfTokenManager wird das als randomize und derandomize bezeichnet.
Es gibt also beliebig viele verschlüsselte Tokens, die alle gültig sind und mit dem Token in der Benutzersitzung verifiziert werden können. Daher kann ein Token beliebig oft verwendet werden, halt so lange die Benutzersitzung gültig ist, bzw. den gleichen Token behält.

Trotzdem sind nicht alle CSRF Tokens für alle Formulare gültig, denn ein Token hat nicht nur einen Wert, sondern auch eine Token-ID, die eindeutig für das jeweilige Formular sein sollte, siehe TokentestType.php. Diese ID wird bei der Token-Validierung ebenso berücksichtigt. Man kann also nicht den token für das Formular "Passwort aktualisieren" in dem Formular "E-Mail Adresse ändern" verwenden. In der Benutzersitzung sollte für jedes Formular ein eigener Token hinterlegt sein.

Session Aware Form Bot

Ein Session CSRF Token kann also beliebig oft innerhalb einer Session verwendet werden. Damit lässt sich einfach ein Bot erzeugen, der Formulardaten beliebig oft übermittelt, z.B. um Schwachstellen in der Formularverarbeitung zu entdecken, oder irgendwelche Angriffe vorzubereiten oder möglicherweise auch anzuwenden. Alles was es dafür benötigt, ist eine Benutzersitzung - also ein Session-Cookie - und einen initialen CSRF Token, um die gewünschten Formulardaten zu senden. Das können wir alles mit curl scripten. Wir müssen für mein Beispiel jedoch einmal die Struktur des Formulars untersuchen, verstehen welche Daten übermittelt werden sollen und/oder müssen, und wie wir den CSRF token identifizieren. Im folgenden Beispiel finden wir den Token mit der ID "tokentest__token" in den Formulardaten:

sessiontokenformbot.sh
#!/bin/sh

TOKEN=$(curl -s --cookie-jar cookies.txt 127.0.0.1:8000 | grep tokentest__token | cut -d'"' -f56 )
for i in {1..10}; do
  curl --cookie cookies.txt -X POST -d "tokentest[acceptTerms]=1&tokentest[someText]=${i}&tokentest[_token]=${TOKEN}" 127.0.0.1:8000
done

In Zeile 3 wird ein initialer GET Request erzeugt, der einerseits den Sitzungs-Cookie in die Datei cookies.txt schreibt, andererseits auch gleichzeitig den CSRF Token aus dem Formular extrahiert. Damit wird das Formular dann 10-mal abgeschickt, wobei die anderen Formulardaten mehr oder weniger zufällig dazugemischt werden.

Die Rückgabe des curl-Requests interessiert uns eigentlich gar nicht, Hauptsache wir können eine valide Formularübermittlung erkennen, dadurch dass das Formular neu erzeugt wird und keine Exception durch die Formular-Validierung erzeugt wird. Im Prinzip können wir die Ausgabe des Scripts auch verwerfen und sogar die Requests asynchron erzeugen, um Zeit zu sparen.

host:~/csrf-token-testsh sessiontokenformbot.sh
...
Submitted token:  a2f83062aad90b7e3.UKzDK99sGWLi0XnlgSitQm0PeeZq2u-DnSLDmIoo5cQ.aIHzZ7RfTBHXqyuHsXD1JjJ8CbY_sL7J9UipyN8Yl4MJ4K5flDNtIaSOTQ
...
Submitted token:  a2f83062aad90b7e3.UKzDK99sGWLi0XnlgSitQm0PeeZq2u-DnSLDmIoo5cQ.aIHzZ7RfTBHXqyuHsXD1JjJ8CbY_sL7J9UipyN8Yl4MJ4K5flDNtIaSOTQ
...
Submitted token:  a2f83062aad90b7e3.UKzDK99sGWLi0XnlgSitQm0PeeZq2u-DnSLDmIoo5cQ.aIHzZ7RfTBHXqyuHsXD1JjJ8CbY_sL7J9UipyN8Yl4MJ4K5flDNtIaSOTQ
...
Submitted token:  a2f83062aad90b7e3.UKzDK99sGWLi0XnlgSitQm0PeeZq2u-DnSLDmIoo5cQ.aIHzZ7RfTBHXqyuHsXD1JjJ8CbY_sL7J9UipyN8Yl4MJ4K5flDNtIaSOTQ
...
Submitted token:  a2f83062aad90b7e3.UKzDK99sGWLi0XnlgSitQm0PeeZq2u-DnSLDmIoo5cQ.aIHzZ7RfTBHXqyuHsXD1JjJ8CbY_sL7J9UipyN8Yl4MJ4K5flDNtIaSOTQ
...
Submitted token:  a2f83062aad90b7e3.UKzDK99sGWLi0XnlgSitQm0PeeZq2u-DnSLDmIoo5cQ.aIHzZ7RfTBHXqyuHsXD1JjJ8CbY_sL7J9UipyN8Yl4MJ4K5flDNtIaSOTQ
...
Submitted token:  a2f83062aad90b7e3.UKzDK99sGWLi0XnlgSitQm0PeeZq2u-DnSLDmIoo5cQ.aIHzZ7RfTBHXqyuHsXD1JjJ8CbY_sL7J9UipyN8Yl4MJ4K5flDNtIaSOTQ
...
Submitted token:  a2f83062aad90b7e3.UKzDK99sGWLi0XnlgSitQm0PeeZq2u-DnSLDmIoo5cQ.aIHzZ7RfTBHXqyuHsXD1JjJ8CbY_sL7J9UipyN8Yl4MJ4K5flDNtIaSOTQ
...
Submitted token:  a2f83062aad90b7e3.UKzDK99sGWLi0XnlgSitQm0PeeZq2u-DnSLDmIoo5cQ.aIHzZ7RfTBHXqyuHsXD1JjJ8CbY_sL7J9UipyN8Yl4MJ4K5flDNtIaSOTQ
...
Submitted token:  a2f83062aad90b7e3.UKzDK99sGWLi0XnlgSitQm0PeeZq2u-DnSLDmIoo5cQ.aIHzZ7RfTBHXqyuHsXD1JjJ8CbY_sL7J9UipyN8Yl4MJ4K5flDNtIaSOTQ
...

Um das nochmal einzuordnen: Alle öffentliche zugängliche Formulare stellen sowieso ein Sicherheitsrisiko dar. Alle Daten, die irgendwie persistiert werden, sei es über eine Datenbank, über E-Mails, über Logfiles, über Schnittstellen, externe Requests o.ä., sind ein wahrscheinlicher Angriffsvektor. Auch wenn öffentliche Formulare wie dieses nicht für CSRF Angriffe geeignet sind, dazu später mehr.

Invalidierung des CSRF Tokens

Es ist einfach, aus einem Session-Token einen One-Time Token zu machen. Alles was es dafür benötigt, ist ein call auf die Methode refreshToken oder removeToken des CsrfTokenManager. Dadurch wird der "Session token" neu erzeugt und alle Requests mit einem "alten" Token werden bei der Validierung einen Fehler erzeugen. Diese Zeile können wir in dem Controller einbauen, beispielsweise nach erfolgreicher Übermittlung eines Tokens, hier Zeile 44:

CsrfTokenTestController.php
<?php
namespace App\Controller;

use App\Entity\Tokentest;
use App\Exception\InvalidCsrfTokenException;
use App\Form\Type\TokentestType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage;

class CsrfTokenTestController extends AbstractController
{
    /**
     * @param Request $request
     * @param CsrfTokenManagerInterface $tokenManager
     * @param SessionInterface $session
     *
     * @return Response
     *
     * @throws InvalidCsrfTokenException
     */
    public function home(Request $request, CsrfTokenManagerInterface $tokenManager, SessionInterface $session): Response
    {
        $submittedToken = '';

        $entity = new Tokentest();
        $entity->setSomeText('default text');
        $entity->setAcceptTerms(true);

        $form = $this->createForm(TokentestType::class, $entity);
        $form->handleRequest($request);

        if ($form->isSubmitted()) {
            $submittedToken = $request->request->get(TokentestType::TOKEN_ID)[TokentestType::TOKEN_NAME] ?? 'invalid';
            if (!$this->isCsrfTokenValid(TokentestType::TOKEN_ID, $submittedToken)) {
                throw new InvalidCsrfTokenException(sprintf(
                    'Invalid CSRF token error: %s',
                    $form->getErrors()->current()->getMessage()
                ));
            }

            $tokenManager->refreshToken(TokentestType::TOKEN_ID);
        }

        $data = [
            'submitted_token' => $submittedToken,
            'session_token' => $session->get(SessionTokenStorage::SESSION_NAMESPACE . '/' . TokentestType::TOKEN_ID),
        ];

        return $this->renderForm('home/form.html.twig', [
            'form' => $form,
            'data' => $data,
        ]);
    }
}

Damit wird erreicht, dass sich bei jeder erfolgreichen Formular-Übertragung der Session token aktualisiert. Die "alten" Tokens werden damit ungültig. Man kann das Formular nicht mehr mit dem gleichen CSRF Token mehrfach übermitteln, sondern erhält in dem Fall eine Fehlermeldung.

csrftokentest-0

Und das script funktioniert nicht mehr :(

host:~/csrf-token-testsh sessiontokenformbot.sh
...
<!-- Invalid CSRF token error: The CSRF token is invalid. Please try to resubmit the form. (500 Internal Server Error) -->
...
...

Den Bot dürfte das jedoch kaum irritieren, denn er kann sich jederzeit ein frisches Formular aufrufen und einen neuen validen CSRF Token erhalten. Das ist genau die gleiche kleine technische Hürde, die wir schon einmal genommen haben, und schon ist das ganze Konstrukt mit dem CSRF Token ausgehebelt. Zeit einmal einen Schritt zurückzutreten und zu schauen, was hier eigentlich passiert und was überhaupt erreicht werden soll.

Öffentliche Formulare und authentifizierte Benutzersitzung

Mit dem Test und dem Formular sollte deutlich werden, was der Unterschied zwischen einem Session-Token und einem One-Time Token ist. Allerdings benutzen wir für unseren Testfall ein "öffentliches" Formular, eines das ohne Benutzerauthentifizierung direkt über das Internet erreichbar ist. Das geht jedoch völlig am Anwendungsfall eines CSRF Tokens vorbei. CSRF Tokens sind für öffentliche Formulare in der Regel völlig sinnlos. Ein CSRF Angriff basiert darauf, dass ein authentifizierter Benutzer Formulare an einen Service übermittelt, die innerhalb einer bereits authentifizierten Sitzung laufen. Eben diese authentifizierte Sitzung wird angegriffen.

Für diese authentifizierte Benutzersitzung ist es eben erforderlich, dass sich der Benutzer vorher angemeldet hat und eine aktive Sitzung mit einem gültigen Sitzungs-Cookie hat. Genau das kann nämlich der Angreifer nicht und versucht das durch den CSRF Angriff zu umgehen.

Bei öffentlichen Formularen gibt es auch ggf. eine Sitzung, die muss jedoch nicht unbedingt authentifiziert sein. Z.B. ein Registrierungs-Dialog wird normalerweise von nicht-authentifizierten Benutzern aufgerufen, das ist für einen CSRF Angriff wahrscheinlich sinnlos. Es gibt sicherlich auch öffentliche Formulare, die auch von authentifizierten Benutzern aufgerufen werden. Diese dürften jedoch nicht in der Form relevante Funktionen bedienen, dass sich ein CSRF Angriff lohnen würde. Wenn doch, sollte man wahrscheinlich noch einmal genauer hinzuschauen. Relevant sind Funktionen, die Benutzerdaten manipulieren, Transaktionen wie Banküberweisungen tätigen, oder irgendwelche andere Schnittstellen aufrufen, die Administrator-Rechte, persönliche Einstellungen und Berechtigungen oder erhöhte Privilegien erfordern. Es wird sicherlich auch öffentliche Formulare geben, die sich für einen angemeldeten Administrator anders verhalten und möglicherweise eine Zwei-Faktor-Authentifizierung umgehen oder Ähnliches, das lasse ich hier aber außen vor.

Das was der sessiontokenformbot.sh oben macht, ist kein CSRF Angriff. Es ist vielleicht Spammen oder vielleicht Ausspionieren, aber mit einem CSRF Token bekommt man diese Art Angriff auf ein öffentliches Formular nicht in den Griff. Vielleicht würde ein Rate Limiter oder eine Firewall in der Lage sein, diesen Formularmissbrauch zu erkennen und auf Netzwerk-Ebene zu unterbinden.

Update 14.08.2022

Ich wurde darauf hingewiesen, dass von Symfony empfohlen wird, auch alle öffentlichen Formulare durch einen CSRF Token abzusichern. In dem Zusammenhang ist besonders ein spezieller Angriff auf das öffentliche Login-Formular erwähnenswert: "Login CSRF". Dabei wird ein Benutzer unwissentlich mit den vorher angelegten Benutzerdaten eines Angreifers auf einer Webseite angemeldet, wo er dann möglicherweise sensible Informationen oder Aktivitäten preisgibt. Auch das ist auf den englischen Wikipedia-Seiten zu Cross-site request forgery unter dem Abschnitt Forging login requests beschrieben, oder auch in einem Artikel auf detectify.com. Auch dieser Angriff kann mit einem CSRF Token unterbunden werden. Danke für den Hinweis!

Auswertung

Gegen CSRF Angriffe sind CSRF Tokens die empfohlene Maßnahme, davon abzuweichen ist unklug. Ein CSRF Angriff auf ein öffentliches Formular ist jedoch unwahrscheinlich. Und wenn man doch solche Angriffe feststellt, sollte man die Anwendungs-Architektur infrage stellen. Aus der Perspektive eines White-box-Testers, also Anwendungstests, bei denen der Sourcecode bekannt ist, würde man sicherlich feststellen, dass die "öffentlichen" von den "authentifizierten" Formularen getrennt werden müssen. Dennoch würde man mit dem CSRF Token auch in öffentlichen Formularen das erreichen, was der CSRF Token kann, nämlich die authentifizierte Benutzersitzung schützen.

Nachvollziehbar ist auch, dass ein CSRF Token nicht immer und unbedingt nur für eine einzige Formularübermittlung gültig sein muss, sondern durchaus so lange gültig sein kann, wie es die Benutzersitzung ist.

Jedoch wenn man einen One-Time Token braucht oder erwartet, machen das die Symfony Forms nicht automatisch. Es reicht jedoch lediglich ein Methodenaufruf, um genau das Verhalten zu erreichen.

Zusammenfassung

Die Symfony Forms erzeugen automatisch einen CSRF Token, der jedoch kein One-Time Token ist, sondern ein session-token. Das ist sinnvoll und kompatibel zu vielen Anwendungsfällen, bei denen ein One-Time Token kompliziert wird. Wer jedoch einen One-Time Token in den Formularen benötigt, muss sich um die Invalidierung der CSRF Tokens selber kümmern.

Schickt Kommentare oder Anmerkungen bitte über Twitter an @limitland oder @jlue.

Referenzen

Symfony security-csrf bundle: https://github.com/symfony/security-csrf
Symfony CSRF protection HOWTO: https://symfony.com/doc/current/security/csrf.html
Symfony Forms Documentation: https://symfony.com/doc/current/forms.html
Wikipedia EN zu CSRF Synchronizer token pattern: https://en.wikipedia.org/wiki/Cross-site_request_forgery#Synchronizer_token_pattern
Wikipedia EN zu CSRF Forging login requests: https://en.wikipedia.org/wiki/Cross-site_request_forgery#Synchronizer_token_pattern
Artikel auf detectify.com über Login CSRF: https://support.detectify.com/support/solutions/articles/48001048951-login-csrf