Created
January 7, 2017 21:48
-
-
Save novoj/504b3cb80ebc31e31ad58f3892bae4aa to your computer and use it in GitHub Desktop.
CSRF Protection against Breach
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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