Warum noch ein Artikel über Cross-Site Request Forgery?
Cross-Site Request Forgery (kurz CSRF) ist seit etwa 2001 bekannt – immerhin also schon mindestens 16 Jahre. Trotzdem stellen wir bei unseren Penetrationstests immer wieder fest, dass es nach wie vor Probleme gibt, sich tatsächlich vor CSRF zu schützen. Entweder wurde überhaupt kein CSRF-Schutz implementiert oder dieser weist Fehler auf, sodass er wirkungslos ist.
Hauptursache ist, dass das Verständnis für CSRF noch nicht vorhanden ist oder die Funktionsweise nicht richtig klar ist. Mit diesem Artikel will ich nochmal deutlich machen, was ein CSRF-Angriff ist, wie er funktioniert und wie einfach man diese Schwachstelle in den Griff bekommt.
Was ist ein CSRF-Angriff?
Ein Cross-Site Request Forgery ist ein Angriff, bei dem ein Benutzer – ohne sein Wissen – eine Aktion innerhalb einer Applikation ausführt. Man kann sich sehr leicht vorstellen, dass das ein riesiges Problem ist. Zum einen können Angreifer über diesen Umweg Aktionen innerhalb einer Anwendung durchführen, ohne dass diese Zugriff auf die Applikation haben. Zum anderen erscheint in Audit-Logs der aufrufende Benutzer, was im Falle eines Incident Response-Prozesses negative Auswirkungen hat. Nicht nur dauert der Incident Response-Prozess länger, sondern es wird auch erst einmal der „Falsche“ verdächtigt.
Was sind die Voraussetzungen für einen funktionierenden CSRF-Angriff?
Grundsätzlich ist es eher aufwändig, einen CSRF-Angriff praktisch durchzuführen, da mehrere Vorrausetzungen erfüllt sein müssen:
- Dem Angreifer muss der entsprechende Aufruf bekannt sein.
- Der angegriffene Benutzer muss in der Applikation eingeloggt sein und über die entsprechenden Rechte verfügen.
- Das Opfer muss auf eine Seite surfen, die vom Angreifer kontrolliert wird, oder auf einen präparierten Link klicken.
- Der entsprechende Aufruf darf nicht über einen CSRF-Schutz verfügen.
Wie ist der Ablauf bei einem CSRF-Angriff?
Zuallererst muss der Aufruf an die Applikation bekannt sein. Vorab noch einmal eine ganz wichtige Information: CSRF funktioniert unabhängig davon, ob HTTP-GET oder -POST verwendet wird! Dies möchte ich nochmal ausdrücklich betonen, da es hier schon öfter Missverständnisse gegeben hat.
Folgender Aufruf verdeutlicht den Angriff. Dies ist bewusst über POST gelöst, damit nicht immer der typische GET-Aufruf verwendet wird.
POST /account/transfer HTTP/1.1 Host: evilbank.com Content-Length: 54 from_account=12233456&to_account=66666666&amount=10000
Das ist die verkürzte Form eines HTTP-Aufrufs, einer Demo-Online-Banking-App, der 10.000 € (Parameter amount) von dem Konto 1223346 (Parameter from_account) auf das Konto 66666666 (Parameter to_account) überweist.
Da kein besonderer Schutzmechanismus vorhanden ist (Details siehe später), ist der Aufruf anfällig für Cross-Site Request Forgery. Ein Angreifer, der dies ausnutzen möchte, geht folgendermaßen vor: Im ersten Schritt wird der Schadcode erzeugt, der die Abfrage absendet. Da der Aufruf über POST erfolgen soll, hat er die beiden Möglichkeiten, dies per XMLHttpRequest (AJAX) umzusetzen oder alternativ über ein einfaches Formular, welches per JavaScript abgesendet wird. Zur Demonstration verwenden wir den zweiten Fall:
<html> <body> <form action="https://evilbank.com/money/transfer" id="myCSRFForm" method="post"> <input type="hidden" name="from_account" value="12233456" /> <input type="hidden" name="to_account" value="66666666" /> <input type="hidden" name="amount" value="10000" /> </form> <script type="text/javascript"> csrf_form = document.getElementById("myCSRFForm"); csrf_form.submit(); </script> </body> </html>
Wird diese HTML-Seite aufgerufen, wird die Anfrage ohne Zutun des Benutzers übertragen, da das Abschicken des Formulars über JavaScript erfolgt. Der Angreifer muss also diesen HTML-Code nur irgendwo platzieren, wo das Opfer früher oder später vorbeisurft.
Im Folgenden soll der Ablauf nochmal grafisch dargestellt werden:
- Als erstes manipuliert der Angreifer eine Webseite, von der er glaubt, dass sie das spätere Opfer besuchen wird. Dies kann natürlich auch durch Phishing provoziert werden.
- Das Opfer wird – mit etwas Glück für den Angreifer – die Webseite besuchen und den Webserver um die manipulierte HTML-Seite bitten
- Der manipulierte Server antwortet und hat im Schlepptau den Schadcode, der später die CSRF-Schwachstelle ausnutzen wird.
- Wenn der Browser des Benutzers die manipulierte Seite rendert, wird auch automatisch – und ohne dass es der Benutzer merkt – eine Anfrage an den eigentlichen Server gesendet.
Wie ist das Risiko für CSRF tatsächlich zu bewerten?
Grundsätzlich klassifizieren wir Cross-Site Request Forgery in unseren Berichten mit „Hoch“, also Stufe 4 von 5. Das liegt darin begründet, dass der Schaden enorm ist – unabhängig davon, ob der Angriff leicht durchzuführen ist oder nicht. Wie oben bereits erwähnt können CSRF-Angriffe für Rechte-Erweiterungen oder andere kritische Dinge verwendet werden.
Allerdings steigt die Wahrscheinlichkeit, einen erfolgreichen Angriff durchzuführen, je weiter die Applikation verbreitet ist. Die Aufrufe müssen erstmal bekannt sein, was bei selbst entwickelten Applikationen, die im internen Netz betrieben werden, vor allem Innentäter betrifft. Es ist also durchaus möglich, dass ein erfolgreicher Cross-Site Request Forgery Angriff umgesetzt wird. Bei sehr bekannten Anwendungen (z.B. Online-Shops oder große soziale Netzwerke), oder bei Anwendungen, die sehr häufig installiert werden, sieht die Sache hingegen bedeutend schlimmer aus.
In den letzten Jahren wurden beispielsweise Router sehr häufig angegriffen (siehe z.B. diesen Heise-Artikel). Die Aufrufe für die Konfigurationsoberflächen sind bekannt, einige hatten noch nicht einmal einen Passwort-Schutz. Da kann ein CSRF schon ein richtiges Problem werden.
Wie schützt man sich gegen CSRF-Angriffe am wirtschaftlichsten?
Da gilt es zuerst einmal zu unterscheiden, um welche Art der Anwendung es sich handelt. Man kann sich nämlich anders schützen, je nachdem, ob es einen Server-State (z.B. eine Session) gibt, oder eben nicht (z.B. bei embedded-Geräten).
Schutz mit Server-State
Die meisten modernen Frameworks zur Entwicklung von Web-Applikationen haben schon CSRF-Schutzmechanismen integriert. Das ist die mit Abstand einfachste und damit wirtschaftlichste Methode, seine Anwendung vor CSRF zu schützen. Sollte dies nicht der Fall sein, und es muss selbst ein CSRF-Schutz implementiert werden, ist wie folgt vorzugehen:
Steht ein Session-Management zur Verfügung, ist es das Einfachste, kryptographisch sichere Zufallswerte zu erzeugen (mindestens 16 Bytes). Dieses CSRF-Token wird in der Session des Benutzers gespeichert und zusätzlich als Parameter bei jedem Aufruf, der den Zustand der Applikation ändert, mit übertragen. Die Übertragung des Parameters ist mithilfe von HTTP-POST oder als HTTP-Request-Header möglich. Dieses Vorgehen implementiert das Synchronizer Token Pattern.
Wichtig: Das so erzeugte CSRF-Token darf nicht über GET übertragen werden, da es sonst leicht abhanden kommen könnte (es steht z.B. in Server-Logfiles oder wird bei Copy&Paste mit übergeben). Außerdem darf es nicht ausschließlich als Cookie übertragen werden, da dieses automatisch bei jedem Aufruf mitgesendet wird. Damit wird es auch im Falle eines CSRF-Angriffs automatisiert übertragen.
CSRF-Token pro Request oder pro Session?
Ich habe schon mehrfach die Frage erhalten, ob es besser ist, ein CSRF-Token pro Request oder pro Session zu haben. Ich rate in den meisten Fällen davon ab, ein Token pro Request zu verwenden. Sicherheit hat auch immer mit Einfachheit und Benutzerfreundlichkeit zu tun. Wenn auf einmal der Zurück-Button wegen eines CSRF-Schutzes nicht mehr funktioniert, sind ihre Benutzer sauer. Außerdem: Wenn ihr CSRF-Token sicher generiert wurde, so kann es der Angreifer auch während der Gültigkeit einer Sitzung nicht erraten. Wenn er auf anderem Wege daran kommt (z.B. über ein Cross-Site Scripting / XSS), schützt auch ein Token pro Request nicht.
Nur bei sehr sensiblen Applikationen, bei denen es auf die einzelne Transaktion ankommt, würde ich empfehlen, den Token pro Request zu verwenden. Ein Beispiel hierfür ist wieder das Online-Banking. Für alles andere ist ein sessionweites Token ausreichend sicher.
Schutz ohne Server-State
Haben Sie keine Möglichkeit, Daten über einen Server-State abzuspeichern, beispielsweise im Embedded-Bereich oder bei massiv parallelen Systemen, gibt es einen anderen Ansatz. Dabei wird bei jedem zustandsändernden Aufruf ein Cookie mit einem CSRF-Token übertragen, das auch gleichzeitig als Parameter mitgesendet wird. Nur wenn diese übereinstimmen, nimmt der Server den Request an. Dies ist die Implementierung des „Double Submit Cookie“-Pattern des OWASP.
In meinen Augen muss allerdings sichergestellt werden, dass das Token serverseitig erzeugt wurde. Hätte der Angreifer nämlich über eine andere Schwachstelle die Möglichkeit, Cookies zu setzen (z.B. auf einer anderen Subdomain), könnte er den CSRF-Schutz aushebeln.
Natürlich ist das kein Problem, was nicht schon andere Entwickler auch hatten. PayPal hat deswegen sogar eine Bibliothek (jwt-csrf) entwickelt, die auf JSON Web Token (JWT) aufbaut.
Ist der Einsatz dieser Bibliothek nicht möglich, beispielsweise aufgrund beschränkter Ressourcen, kann folgendes vereinfachtes Verfahren verwendet werden, um ein verifizierbares Token zu erzeugen:
secret = (siehe beschreibung unten) user = (Benutzername, wenn vorhanden) timestamp = (Aktueller Timestamp. Empfohlen: Millisekunden seit epoch) random = (16-Byte kryptographisch sicherer Zufallswert) signature = sha256(user + timestamp + random + secret) token = url_encode(base64(user + timestamp + random + signature)) # auf keinen Fall das Secret!
Das oben erwähnte secret muss aus mindestens 32 Byte bestehen, kryptographisch sicher erzeugt werden und – das ist das allerwichtigste – pro Installation bzw. Gerät einmalig sein. Die einzige(!!!) Ausnahme ist, wenn es ein verteiltes System ist – da muss das secret natürlich systemweit identisch sein.
Mit den Klartext-Informationen von user und timestamp kann die Gültigkeit der Signatur sichergestellt werden. Die Mitgabe des Zeitstempels erlaubt es sicherzustellen, dass Token nur eine bestimmte Zeit gültig sind. Zu empfehlen sind hier ein Zeitraum von 15 Minuten bis zu ein paar wenigen Stunden – je nach Kritikalität der Anwendung, wobei kürzer natürlich grundsätzlich besser ist.
Wie prüfen wir auf CSRF-Anfälligkeit?
Hier eine kleine Zusammenfassung unserer wichtigsten Prüfschritte währen eines Web-Applikations-Penetrationtest, um zu analysieren, ob eine Anwendung für Cross-Site Request Forgery anfällig ist.
- Wird überhaupt ein CSRF-Token übertragen?
- Wird das Token geprüft?
- Was passiert beim Übertragen eines leeren Tokens?
- Kann das Token vorhergesagt werden?
- Bei Double Submit: Kann das Cookie vorgegeben werden?
Das sind erstmal die wichtigsten Punkte. Je nachdem, was uns dann noch in der Implementierung auffällt, führen wir noch weitere, manuelle Schritte aus.
Weitere Ressourcen
Wie immer gibt es ein hervorragendes Prevention Cheat Sheet des OWASP Projekts, das immer einen Blick lohnt.
Konkrekte Implementierungen für verschiedene Programmiersprachen:
- ASP.NET
- Java mit OWASP CSRFGuard
- Java mit Spring
- Python mit Django
- Ruby on Rails
- PHP muss leider selbst entwickelt werden, gute Grundlage ist hier zu finden.
Das ist natürlich nur eine kleine Auswahl. Prüfen Sie, ob bei dem von Ihnen verwendeten Framework bereits ein CSRF-Schutz mitgeliefert wird. Dies ist immer noch der einfachste Weg, um Ihre Anwendung gegen CSRF abzusichern.