Skip to content

Instantly share code, notes, and snippets.

@novoj
Created January 7, 2017 21:48
Show Gist options
  • Save novoj/504b3cb80ebc31e31ad58f3892bae4aa to your computer and use it in GitHub Desktop.
Save novoj/504b3cb80ebc31e31ad58f3892bae4aa to your computer and use it in GitHub Desktop.
CSRF Protection against Breach
package com.fg.http.csrf;
import org.apache.commons.codec.binary.Base32;
import org.springframework.web.util.WebUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.security.SecureRandom;
import java.util.Random;
/**
* This class contains method for secure CSRF token generation and Breach protection.
*
* @author Jan Novotný ([email protected]), FG Forrest, a.s.
*/
public class CsrfSupport {
private static final SecureRandom RANDOM_GENERATOR = new SecureRandom();
private static final Random FAST_RANDOM_GENERATOR = new SecureRandom();
private static final String CSRF_SESSION_TOKEN = "__CSRF_SESSION_TOKEN";
private static final String REQUEST_CSRF_TOKEN = "__CSRF_REQUEST_TOKEN";
private static final int CSRF_TOKEN_LENGTH = 20;
private static final int CSRF_ENCODED_TOKEN_LENGTH = 32;
private CsrfSupport() { }
/**
* Generates random 20B wide token in a secure way.
* @return
*/
public static String generateRandomToken() {
return generateUniqueToken(20);
}
/**
* Generates secure random token of specified size.
* @param size
* @return
*/
public static String generateUniqueToken(int size) {
final byte[] formId = new byte[size];
RANDOM_GENERATOR.nextBytes(formId);
return new Base32().encodeAsString(formId);
}
/**
* Generates random bytes of specified count with less security requirements for the random generator.
* @param size
* @return
*/
public static byte[] generateRandomBytes(int size) {
final byte[] randomBytes = new byte[size];
FAST_RANDOM_GENERATOR.nextBytes(randomBytes);
return randomBytes;
}
/**
* Returns CSRF token from session if session already exists.
*
* @param request
* @return valid CSRF token if session exists, null if it does not
*/
public static String getCsrfToken(HttpServletRequest request) {
if(request.getSession(false) == null) {
return null;
} else {
return getCsrfToken(request.getSession());
}
}
/**
* Returns CSRF token from session or initializes new one if it hasn't been yet created.
*
* @param session
* @return valid CSRF token
*/
public static String getCsrfToken(HttpSession session) {
final String csrfToken = (String)session.getAttribute(CSRF_SESSION_TOKEN);
if(csrfToken == null) {
return initCsrfToken(session);
} else {
return csrfToken;
}
}
/**
* Variant of {@link #encryptCsrfToken(String)} that involves caching of computed value to request attribute.
* @param request
* @return
*/
public static String getEncryptedCsrfToken(HttpServletRequest request) {
String encryptedToken = (String)request.getAttribute(REQUEST_CSRF_TOKEN);
if (encryptedToken == null) {
encryptedToken = encryptCsrfToken(getCsrfToken(request));
request.setAttribute(REQUEST_CSRF_TOKEN, encryptedToken);
}
return encryptedToken;
}
/**
* This method allows to generate pseudo random string that masks original CSRF token that doesn't change for
* entire session. Original CSRF token can be easily decrypted from the random string by XORing left part with
* the right one.
*
* This technique allows to mitigate Heist/Breach attacks that allow attacker to guess CSRF token very fast.
* By changing token contents on every request the attack is (for CSRF token only) mitigated.
*
* @return
*/
public static String encryptCsrfToken(String csrfToken) {
final Base32 base32 = new Base32();
final byte[] salt = generateRandomBytes(CSRF_TOKEN_LENGTH);
final byte[] csrf = base32.decode(csrfToken);
final int csrfLength = csrf.length;
final byte[] encrypted = new byte[csrfLength];
for(int i = 0; i < csrfLength; i++) {
int c = csrf[i];
int s = salt[i];
encrypted[i] = (byte)(0xff & (c ^ s));
}
return base32.encodeAsString(encrypted) + base32.encodeAsString(salt);
}
/**
* Decrypts CSRF token encrypted with {@link #encryptCsrfToken(String)}
* @param csrfToken
* @return
*/
public static String decryptCsrfToken(String csrfToken) {
if (csrfToken.length() == CSRF_ENCODED_TOKEN_LENGTH) {
// non-encrypted variant passed in input
return csrfToken;
} else {
final Base32 base32 = new Base32();
final byte[] csrf = base32.decode(csrfToken.substring(0, CSRF_ENCODED_TOKEN_LENGTH));
final byte[] salt = base32.decode(csrfToken.substring(CSRF_ENCODED_TOKEN_LENGTH, csrfToken.length()));
final int csrfLength = csrf.length;
for(int i = 0; i < csrfLength; i++) {
byte c = csrf[i];
byte s = salt[i];
csrf[i] = (byte)(0xff & (c ^ s));
}
return base32.encodeAsString(csrf);
}
}
/**
* Initializes CSRF token in session in case it hasn't already exist.
*
* @param session
* @return valid CSRF token
*/
public static String initCsrfToken(HttpSession session) {
final Object mutex = WebUtils.getSessionMutex(session);
synchronized(mutex) {
final String token = generateUniqueToken(CSRF_TOKEN_LENGTH);
if(session.getAttribute(CSRF_SESSION_TOKEN) == null) {
session.setAttribute(CSRF_SESSION_TOKEN, token);
return token;
} else {
return (String)session.getAttribute(CSRF_SESSION_TOKEN);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment