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

Seite 2: Token

Inhaltsverzeichnis

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 (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.