zurück zum Artikel

Sichere Java-Webanwendungen, Teil 2: Cross-Site Request Forgery

Dominik Schadow

Eine durch Cross-Site Request Forgery (CSRF) verwundbare Webanwendung ermöglicht es dem Angreifer, einem Benutzer heimlich Requests unterzuschieben und sie von ihm ausführen zu lassen. Die Protokolle der Webanwendung enthalten dabei gewöhnlich keinen Hinweis auf einen erfolgten Angriff.

Sichere Java-Webanwendungen, Teil 2: Cross-Site Request Forgery

Eine durch Cross-Site Request Forgery (CSRF) verwundbare Webanwendung ermöglicht es dem Angreifer, einem Benutzer heimlich Requests unterzuschieben und sie von ihm ausführen zu lassen. Die Protokolle der Webanwendung enthalten dabei gewöhnlich keinen Hinweis auf einen erfolgten Angriff.

Normalerweise sind bei einer Webanwendung ankommende Requests – ob nun GET oder POST ist irrelevant – vom jeweiligen Benutzer willentlich ausgelöst: Ein Klick auf "Set Administrator" zum Upgrade des ausgewählten Benutzers in die Gruppe der Administratoren, das Ausfüllen des Formulars zum Anlegen eines neuen Nutzers oder beliebige andere Aktionen, die der korrekt authentifizierte und autorisierte Anwender im Browser auslöst.

Damit der Benutzer (bzw. der zuvor erwähnte Administrator) derartige Operationen durchführen darf, muss er sich zunächst bei der Webanwendung anmelden. Bei korrektem Benutzernamen und Passwort erhält er anschließend eine Session-ID. In Java heißt der entsprechende Parameter standardmäßig JSESSIONID und hat beispielsweise den zufälligen Wert 55F770CDC57C3F396FA66D376FE643B4. Dieser wird meist im ebenfalls JSESSIONID genannten Cookie gespeichert – hin und wieder allerdings auch als URL-Parameter. Damit das Backend der Webanwendung den Benutzer wiedererkennt (HTTP ist schließlich zustandslos), überträgt der Browser die Session-Informationen bei jedem Request automatisch ans Backend.

Diesen Umstand nutzt ein Angreifer aus und präpariert eine spezielle Webseite zum Angriff auf die ausgewählte Webanwendung. Die erstellte Seite, etwa ein Forum oder ein Blog, ist unter einer beliebigen Domain erreichbar, und muss sich nicht auf dem Server der als Ziel ausgewählten Webanwendung befinden. Um Nutzer auf das Webangebot des Angreifers zu locken, kommen häufig Formen des Social Engineering zur Anwendung. Ein Beispiel hierfür ist ein Forum, in dem Probleme der anzugreifenden Webanwendung diskutiert werden. Benutzer, vor allem aber Administratoren, will man durch derartige Inhalte zum Besuch der Angreifer-Website verleiten. In deren HTML-Code enthalten ist zum Beispiel ein per JavaScript mit XmlHttpRequest (XHR) automatisch beim Laden ausgeführter POST-Request, mit dem die Anwendung einen neuen Benutzer anlegt:

<script type="text/javascript">
function sendForm() {
var request = new XMLHttpRequest();
request.open("POST",
"http://www.site.com/admin/AdminServlet");
request.setRequestHeader("Content-type",
"application/x-www-form-urlencoded");
request.send("userid=1234&username=
Webadmin&password=Webadmin1&active=true");
}
</script>

Die bekannte Same-Origin Policy [1] verhindert hier nur, dass die Anwendung eine Response zum Request zurückliefert. Das Auslösen des Requests über Domaingrenzen hinweg ist dagegen problemlos möglich. Da der Angreifer bei einem CSRF-Angriff aber ohnehin nicht an einer Antwort der Webanwendung interessiert ist, stört solch eine Einschränkung nicht weiter.

Eine weitere Seite (idealerweise eine Folgeseite) präpariert der Angreifer mit einem simplen GET-Request, mit dem der zuvor angelegte Benutzer in die Gruppe der Administratoren aufgenommen wird:

<img src="http://www.site.com/admin/AdminServlet
?userid=1234&group=admin" width="0" height="0">

Der Code ist deutlich einfacher als der erste Teil zum Anlegen des Benutzers und benötigt ausschließlich HTML. Der Browser zeigt dem Benutzer durch die jeweils auf 0 festgelegte Breite und Höhe kein Broken-Image an. Das Bild ist einfach unsichtbar.

Ein Besuch auf der vom Angreifer erstellten Webseite löst nun immer den XmlHttpRequest per JavaScript aus. Sofern der Besucher nicht bei der Webanwendung angemeldet ist, und damit nicht über eine gültige Session verfügt, bricht die Anwendung die Request-Verarbeitung ab. Zum Abbruch führt ebenfalls ein Besucher mit gültiger Session, der allerdings nicht zu der in dem Fall festgelegten Administratorengruppe gehört (d.h., er verfügt nicht über ausreichende Rechte). Bei einem Administrator wird der Request dagegen verarbeitet und der neue Benutzer angelegt. Ähnliches gilt für Besucher der zweiten Seite mit der versteckten Grafik: Sie führt beim Besuch die Zuordnung des neuen Benutzers zur Administratorengruppe durch.

Der ausgenutzte Administrator bekommt davon nichts mit. Auch in der Webanwendung deutet wenig auf einen Angriff hin. Die Logs enthalten gegebenenfalls den Hinweis, dass ein neuer Benutzer angelegt und der Administratorengruppe hinzugefügt wurde und welcher legitime Benutzer (Administrator) Auslöser des Vorgangs war.

Mangels Rückmeldung der Webanwendung kann der Angreifer den Erfolg seines Angriffs nur durch einen Anmeldeversuch mit dem durch das Skript angelegten Benutzer überprüfen. Eine direkte Response der Webanwendung erreicht ihn nie.

Je nach angegriffener Webanwendung lassen sich unterschiedlichste Operationen auslösen. Hier hängt es davon an, wie lange die Benutzerinformationen aus dem Cookie der Webanwendung genügen und wann sie die Eingabe des Benutzerpassworts vor dem Ausführen einer Operation erfordert. Neben dem Anlegen von Benutzern für spätere Angriffe können Angreifer beispielsweise auch Bestellungen auslösen oder Daten manipulieren. Lediglich der Datendiebstahl, wie er bei SQL Injections möglich ist, stellt keine Option dar, da die Anwendung keine Daten an den Angreifer zurückliefert.

Noch gefährlicher ist ein Angriff mit CSRF allerdings dadurch, dass er sich als Türöffner ins Intranet verwenden lässt. Der Angreifer nutzt dazu einen dort angemeldeten Benutzer (zum Beispiel per VPN oder im LAN/WLAN des Unternehmens). Beim Besuch der präparierten Webseite führt der Browser des Benutzers die Requests ins Intranet durch. Eine gewöhnliche Firewall kann den Angriff nicht aufhalten, lediglich mit einer speziellen und korrekt konfigurierten Web Application Firewall würde der Request gestoppt. Dass solche per CSRF durchgeführte Angriffe immer noch weit verbreitet sind, zeigt etwa deren Platz 8 in den aktuellen OWASP Top 10 [2]. Höchste Zeit also, etwas gegen diese Bedrohung zu unternehmen.

Die beiden Anfangsbeispiele haben gezeigt, dass ein Angreifer zwei Dinge für einen Angriff per CSRF wissen muss: Die URL, an die der Request zu schicken ist, und die dabei notwendigen Parameter. Bei Anwendungen im Internet lassen sich derartige Informationen meist vergleichsweise einfach in Erfahrung bringen. Der Angreifer kann sich hierzu häufig selbst einen Standardaccount anlegen und so die Anwendung ausspionieren. Webanwendungen im Intranet eines Unternehmens erhöhen den Spionageaufwand. Hier fällt die Recherche bei Standardanwendungen wiederum leichter als bei Individualsoftware. Hundertprozentigen Schutz bietet eine Geheimhaltung allerdings nicht. Ex-Mitarbeiter, externe Entwickler und andere Informationsquellen könnten die notwendigen Daten verraten. Als Webentwickler muss man daher stärkere Geschütze gegen die CSRF-Bedrohung auffahren und Webanwendungen unabhängig ihres Einsatzortes vor CSRF schützen.

Wie eingangs gezeigt, lassen sich auch POST-Requests per CSRF auslösen. Allerdings ist der Aufwand dafür höher. Gleichzeitig halten sich Entwickler so ans REST-Paradigma [3], wonach GET-Requests keine Zustandsveränderung im Backend auslösen und lediglich Informationen zurückliefern dürfen. Wo immer Formulare zum Datenversand verwendet werden und im Backend eine Verarbeitung auslösen, sollte daher die POST-Methode zum Einsatz kommen:

<form method="POST" action="AdminServlet">
<!-- ... -->
</form>

Den bestmöglichen Schutz vor CSRF erreicht man mit einem benutzer- und sessionspezifischen und vor allem zufällig berechneten Token. Ihn kann man zu Beginn der Session für den Benutzer berechnen und serverseitig in dessen Session speichern:

public final class CSRFTokenHandler {
public static final String CSRF_TOKEN = "CSRF_TOKEN";

private static String getToken() throws Exception {
SecureRandom sr = SecureRandom.
getInstance("SHA1PRNG", "SUN");
sr.nextBytes(new byte[20]);
return String.valueOf(sr.nextLong());
}

public static String getToken(HttpSession session)
throws Exception {
if (session == null) {
throw new ServletException("No session");
}

String token = (String) session.
getAttribute(CSRF_TOKEN);

if (StringUtils.isEmpty(token)) {
token = getToken();
session.setAttribute(CSRF_TOKEN, token);
}

return token;
}
// ...
}

Gleichzeitig wird das Token jedem kritischen Formular der Webanwendung (d.h. jedem Formular, das eine Zustandsveränderung auslöst) als versteckter Wert hinzugefügt. Das Verbergen dient dabei nicht dem (ohnehin wirkungslosen) Schutz des Tokens, sondern einzig und allein dem Verbergen des für einen Benutzer unverständlichen Konstrukts:

<form name="orderForm" action="OrderServlet"
method="POST">
<input type="hidden"
name="<%=CSRFTokenHandler.CSRF_TOKEN%>"
value="<%=CSRFTokenHandler.getToken
(request.getSession(false))%>">
<!-- ... -->
<input type="submit" value="Order" />
</form>

Nach dem Übermitteln des Formulars muss das Backend den Wert des übermittelten Tokens mit dem auf dem Server gespeicherten vergleichen. Dieses Vorgehen bezeichnet man daher als Synchronizer-Token- Pattern. Nur bei übereinstimmenden Werten führt das Backend den Request anschließend aus, andernfalls bricht es die Verarbeitung ab:

@WebServlet(name = "OrderServlet", urlPatterns = {"/OrderServlet"})
public class OrderServlet extends HttpServlet {
// ...

protected void doPost(HttpServletRequest req,
HttpServletResponse res) throws ServletException {
if (!CSRFTokenHandler.isValid(req)) {
res.setStatus(401);
// ...
return;
}

// Token OK, Requestverarbeitung fortsetzen
}
}

public final class CSRFTokenHandler {
public static final String CSRF_TOKEN = "CSRF_TOKEN";

// ...

public static boolean isValid(HttpServletRequest req)
throws Exception {
if (req.getSession(false) == null) {
throw new ServletException("No session");
}

return StringUtils.equals(getToken(
req.getSession(false)),
req.getParameter(CSRF_TOKEN));
}
}

Löst nun ein Angreifer heimlich einen Request per CSRF aus, fehlt dieses Token. Die Webanwendung erkennt daran, dass der Request gefälscht ist und weist ihn ab.

Auch wenn sich solch ein Schutz verhältnismäßig einfach selbst entwickeln lässt, sollte man dafür immer ein Framework oder eine spezialisierte Java-Bibliothek verwenden. Das Beispiel zuvor basiert auf der Enterprise Security API [4] (ESAPI). Weitere Frameworks werden im Verlauf des Artikels noch vorgestellt.

Der CSRF-Schutz lässt sich auf zwei Arten umsetzen: Die erste sieht vor, das Token nach jedem Request neu zu berechnen und damit den Schutz zu erhöhen. Die meisten Frameworks mit integriertem CSRF-Schutz gehen diesen Weg. Selbst ein durch einen GET-Request bekannt gewordenes Token ist für den Angreifer somit nutzlos, da sich dessen gültiger Wert bereits geändert hat.

Die zweite Variante wird als Double-Submit-Pattern bezeichnet. Hierbei kann das Backend zustandslos, das heißt ohne Session, arbeiten. Das zuvor in der Session gespeicherte Token landet dabei in einem eigenen Cookie (niemals im Session-Cookie), das Formular-Token im HTML bleibt unverändert erhalten. Die initiale Berechnung des Tokens findet weiterhin im Backend statt. Anschließend hat letzteres bei jedem eingehenden Request nur noch die Aufgabe, beide vom Client übermittelten Token-Werte – den aus dem Formular und den im Cookie – zu vergleichen. Zwar überträgt der Browser das Token-Cookie auch bei einem gefälschten Request automatisch, allerdings fehlt das Token in den Formulardaten. Der CSRF-Schutz ist damit auch in dieser Variante gewährleistet. Da Frameworks und Bibliotheken in der Regel allerdings auf dem Synchronizer-Token-Pattern basieren, muss man für die Variante üblicherweise zumindest etwas mehr selbst entwickeln.

Welche Vorgehensweise die geeignetere ist, macht man idealerweise an der Session fest: Sofern sie im Backend vorhanden ist, sollte man das Synchronizer-Token-Pattern verwenden. Nur wenn das nicht der Fall ist, sollte man auf das Double-Submit-Pattern zurückgreifen.

Eigentlich fallen Log-in- und Log-out-Formulare nicht in die Kategorie kritischer (zustandsverändernder) Formulare. Die Notwendigkeit, sie vor CSRF zu schützen, wird daher häufig übersehen. Log-in-Formulare vor CSRF abzuschirmen ist nötig, damit sich Benutzer nicht mit ihnen untergeschobenen Benutzerdaten anmelden können. Ein Angreifer könnte sonst einen Anwender heimlich mit seinen für den Angriff angelegten Benutzerdaten anmelden und dessen ausgeführte Operationen exakt nachvollziehen. Je nach Webanwendung gibt der Benutzer bei der Interaktion mit der Webanwendung unterschiedliche sensible Informationen preis – etwa Kreditkarteninformationen für eine Bestellung – die der Angreifer anschließend einsehen kann.

Log-out-Formulare benötigen hingegen Schutz, damit Benutzer nicht ungewollt abgemeldet und wie im Abschnitt zuvor beschrieben mit den Benutzerdaten des Angreifers wieder angemeldet werden können. Denkbar ist zudem, den Benutzer nach der Abmeldung sofort auf eine vom Angreifer manipulierte Log-in-Seite umzuleiten. Die dort eingegebenen Benutzerdaten werden vom Angreifer abgefangen und später von ihm wiederverwendet. Daher muss auch ein Log-out-Formular einen POST-Request auslösen und nicht nur einen simplen GET-Request per Log-out-Link.

Wie jede clientseitige Information ist das Anti-CSRF-Token prinzipiell per Cross-Site Scripting (XSS) [5] auslesbar. Eine derart verwundbare Webanwendung lässt sich daher nicht zuverlässig vor Cross-Site Request Forgery schützen. Ein Angreifer könnte schließlich zunächst den Wert des Anti-CSRF-Token aus dem Formular auslesen und ihn im eigenen Request verwenden. Für einen zuverlässigen Schutz vor CSRF ist es daher unabdingbar, dass die Webanwendung zunächst vor XSS geschützt ist.

Da sich die wichtigste Maßnahme gegen CSRF – das Anti-CSRF-Token – relativ leicht in Frameworks integrieren lässt, ist sie immer häufiger in deren Code enthalten. JavaServer Faces (JSF) etwa schützen schon geraume Zeit vor CSRF und mit der zuletzt veröffentlichten Version 2.2 gab es erneute Verbesserungen. Die Funktion des Anti-CSRF-Tokens übernimmt dabei das javax.faces.ViewState-Token, das ohne Zutun des Entwicklers in jedem Formular enthalten ist und sich nach jedem Request ändert (im folgenden Screenshot wurde das versteckte Token per Browser-Add-on sichtbar gemacht):

Automatisch von JSF generiertes Token zum Schutz vor CSRF-Angriffen (Abb. 1).

Automatisch von JSF generiertes Token zum Schutz vor CSRF-Angriffen (Abb. 1).

Seit Version 3.2 enthält Spring Security [6] ebenfalls einen einfach zu verwendenden CSRF-Schutz. Auch hier ändert sich der Token-Wert mit jedem Request. Der CSRF-Schutz ist beim Einsatz einer Java-Konfiguration automatisch immer aktiv. Lediglich bei Verwendung der XML-Konfiguration ist er zumindest derzeit noch explizit durch das Hinzufügen des csrf-Elements zu zu aktivieren:

<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
<!-- ... -->
<http>
<!-- ... -->
<csrf />
</http>
</beans>

Des Weiteren müssen Entwickler die gewünschten Formulare meist durch ein Anti-CSRF-Token erweitern (bei Verwendung des Spring MVC beispielsweise wird das Token dem Formular automatisch hinzugefügt):

<form name="orderForm" action="OrderServlet" method="POST">
<input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}"/>
<label for="product" title="Product">Product</label>
<input type="text" id="product" name="product" />
<!-- ... -->
<input type="submit" value="Order" />
</form>

CSRF-Schutzmaßnahmen sollten Entwickler unabhängig vom gewählten Framework grundsätzlich immer im Browser testen. Dazu fälscht man das Anti-CSRF-Token und prüft die Reaktion des Backends. Ein empfehlenswertes (wenn auch nicht mehr weiterentwickeltes) und einfach zu verwendendes Firefox-Browser-Add-on dafür ist Groundspeed [7], mit dem man selbst versteckte Formularwerte schnell manipulieren kann. Deutlich umfangreichere Tools wie der Intercepting-Proxy OWASP ZAP [8] ermöglichen ebenfalls die Manipulation des Tokens nach dem Abschicken des Requests.

Das Überprüfen des Backends ist die zweite wichtige Maßnahme. Hierbei werden die Operationen direkt und ohne den Umweg über den Browser aufgerufen. Bei fehlendem Token ist der Request unmittelbar abzuweisen.

Auch beim Schutz vor Cross-Site Request Forgery gehen viele Webframeworks mittlerweile den richtigen Weg und integrieren einen weitgehend automatischen Schutz. Der Entwickler muss so deutlich weniger für sichere Webanwendungen tun. Fehler oder schlichtes Vergessen des CSRF-Schutzes werden unwahrscheinlicher. Aber selbst mit automatischen Maßnahmen muss man sich als Entwickler der Gefahr von CSRF stets bewusst sein, und den von Frameworks bereitgestellten Schutz kritisch prüfen.

Die vollständigen Sourcen der im Artikel gezeigten Codebeispiele finden sich im GitHub-Repository [9] in den Projekten CSRF und CSRF-spring-security.

Dominik Schadow [10]
arbeitet als Senior Consultant beim IT-Beratungsunternehmen bridgingIT. Er ist auf die sichere Entwicklung von Java-Enterprise-Applikationen spezialisiert und Autor des Buchs "Java-Web-Security: Sichere Webanwendungen mit Java entwickeln".
(jul [11])


URL dieses Artikels:
https://www.heise.de/-2303228

Links in diesem Artikel:
[1] http://www.w3.org/Security/wiki/Same_Origin_Policy
[2] https://www.owasp.org/index.php/Top_10_2013
[3] http://de.wikipedia.org/wiki/Representational_State_Transfer
[4] https://www.owasp.org/index.php/Category:OWASP_Enterprise_Security_API
[5] https://www.heise.de/ratgeber/Sichere-Java-Webanwendungen-Teil-1-Cross-Site-Scripting-2263707.html
[6] http://projects.spring.io/spring-security
[7] https://addons.mozilla.org/en-US/firefox/addon/groundspeed
[8] https://www.owasp.org/index.php/OWASP_Zed_Attack_Proxy_Project
[9] https://github.com/dschadow/JavaSecurity
[10] https://blog.dominikschadow.de
[11] mailto:jul@heise.de